@magic-markdown/cli 0.3.20 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1112 -1624
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -270,13 +270,11 @@ var require_punycode = __commonJS({
|
|
|
270
270
|
});
|
|
271
271
|
|
|
272
272
|
// src/index.ts
|
|
273
|
-
import { readFile as readFile8 } from "node:fs/promises";
|
|
274
273
|
import { resolve as resolve6 } from "node:path";
|
|
275
274
|
|
|
276
275
|
// ../core/src/types.ts
|
|
277
|
-
var
|
|
276
|
+
var WORKSPACE_SCHEMA_VERSION = 1;
|
|
278
277
|
var MANIFEST_PATH = ".mdocs/manifest.json";
|
|
279
|
-
var SIDECAR_DIR = ".mdocs/docs";
|
|
280
278
|
var CHECKPOINT_DIR = ".mdocs/checkpoints";
|
|
281
279
|
|
|
282
280
|
// ../core/src/errors.ts
|
|
@@ -302,6 +300,11 @@ function createId(prefix) {
|
|
|
302
300
|
function createDocId() {
|
|
303
301
|
return createId("doc");
|
|
304
302
|
}
|
|
303
|
+
var DOC_ID_SEPARATOR = "\0";
|
|
304
|
+
function deterministicDocId(rootId, path) {
|
|
305
|
+
const digest = fnv1a(`${rootId}${DOC_ID_SEPARATOR}${path}`).toString(16).padStart(16, "0");
|
|
306
|
+
return `doc_${digest}`;
|
|
307
|
+
}
|
|
305
308
|
function contentHashForText(value) {
|
|
306
309
|
return `text_${fnv1a(value).toString(16).padStart(16, "0")}`;
|
|
307
310
|
}
|
|
@@ -334,10 +337,10 @@ function parseWikilinkBody(value) {
|
|
|
334
337
|
|
|
335
338
|
// ../core/src/markdown.ts
|
|
336
339
|
var POLLUTING_PATTERNS = [
|
|
337
|
-
{ name: "
|
|
338
|
-
{ name: "
|
|
339
|
-
{ name: "
|
|
340
|
-
{ name: "
|
|
340
|
+
{ name: "inline review addition marker", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
|
|
341
|
+
{ name: "inline review deletion marker", pattern: /\{--[\s\S]*?--\}/ },
|
|
342
|
+
{ name: "inline review substitution marker", pattern: /\{~~[\s\S]*?~~\}/ },
|
|
343
|
+
{ name: "inline review comment marker", pattern: /\{>>[\s\S]*?<<\}/ },
|
|
341
344
|
{ name: "mdocs HTML marker", pattern: /<!--\s*mdocs:/i },
|
|
342
345
|
{ name: "mdocs suggestion marker", pattern: /<!--\s*\/?\s*mdocs-suggestion\b/i },
|
|
343
346
|
{ name: "mdocs directive", pattern: /:{1,3}(?:comment|suggestion|anchor)\b/i }
|
|
@@ -355,7 +358,7 @@ function assertCleanMarkdown(markdown) {
|
|
|
355
358
|
throw new MdocsError(
|
|
356
359
|
"validation_error",
|
|
357
360
|
`Markdown contains collaboration markup: ${names}`,
|
|
358
|
-
"Keep Markdown clean; record comments and suggestions through the
|
|
361
|
+
"Keep Markdown clean; record comments and suggestions through the document authority instead of inline markers."
|
|
359
362
|
);
|
|
360
363
|
}
|
|
361
364
|
}
|
|
@@ -368,10 +371,6 @@ function titleFromMarkdown(path, markdown) {
|
|
|
368
371
|
function getLines(markdown) {
|
|
369
372
|
return markdown.length === 0 ? [] : markdown.split(/\r?\n/);
|
|
370
373
|
}
|
|
371
|
-
function extractLineRange(markdown, range) {
|
|
372
|
-
const lines = getLines(markdown);
|
|
373
|
-
return lines.slice(range.startLine - 1, range.endLine).join("\n");
|
|
374
|
-
}
|
|
375
374
|
function assertLineRangeWithin(markdown, range) {
|
|
376
375
|
if (!Number.isInteger(range.startLine) || !Number.isInteger(range.endLine) || range.startLine < 1 || range.endLine < range.startLine) {
|
|
377
376
|
throw new MdocsError(
|
|
@@ -406,7 +405,7 @@ function replacementLinesForRange(replacement) {
|
|
|
406
405
|
function extractMarkdownImages(markdown, documentPath) {
|
|
407
406
|
const masked = maskCode(markdown);
|
|
408
407
|
const referenceDefinitions = collectReferenceDefinitions(markdown, masked);
|
|
409
|
-
const
|
|
408
|
+
const lineStarts = getLineStarts(markdown);
|
|
410
409
|
const images = [];
|
|
411
410
|
let index = 0;
|
|
412
411
|
while (index < masked.length) {
|
|
@@ -424,7 +423,7 @@ function extractMarkdownImages(markdown, documentPath) {
|
|
|
424
423
|
}
|
|
425
424
|
const alt = markdown.slice(labelOpen + 1, labelClose);
|
|
426
425
|
const afterLabel = labelClose + 1;
|
|
427
|
-
const position = positionAt(
|
|
426
|
+
const position = positionAt(lineStarts, start);
|
|
428
427
|
if (masked[afterLabel] === "(") {
|
|
429
428
|
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
430
429
|
if (destinationClose === -1) {
|
|
@@ -492,7 +491,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
492
491
|
const codeMasked = maskCode(markdown);
|
|
493
492
|
const referenceDefinitions = collectReferenceDefinitions(markdown, codeMasked);
|
|
494
493
|
const masked = maskReferenceDefinitions(markdown, codeMasked);
|
|
495
|
-
const
|
|
494
|
+
const lineStarts = getLineStarts(markdown);
|
|
496
495
|
const links = [];
|
|
497
496
|
let index = 0;
|
|
498
497
|
while (index < masked.length) {
|
|
@@ -509,7 +508,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
509
508
|
}
|
|
510
509
|
const label = markdown.slice(start + 1, labelClose);
|
|
511
510
|
const afterLabel = labelClose + 1;
|
|
512
|
-
const position = positionAt(
|
|
511
|
+
const position = positionAt(lineStarts, start);
|
|
513
512
|
if (masked[afterLabel] === "(") {
|
|
514
513
|
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
515
514
|
if (destinationClose === -1) {
|
|
@@ -565,7 +564,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
565
564
|
}
|
|
566
565
|
index = labelClose + 1;
|
|
567
566
|
}
|
|
568
|
-
extractWikilinks(markdown, masked,
|
|
567
|
+
extractWikilinks(markdown, masked, lineStarts).forEach((link2) => links.push(link2));
|
|
569
568
|
return links.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
570
569
|
}
|
|
571
570
|
function toImageReference(image2) {
|
|
@@ -592,7 +591,7 @@ function toLinkReference(link2) {
|
|
|
592
591
|
syntax: link2.syntax
|
|
593
592
|
};
|
|
594
593
|
}
|
|
595
|
-
function extractWikilinks(markdown, masked,
|
|
594
|
+
function extractWikilinks(markdown, masked, lineStarts) {
|
|
596
595
|
const links = [];
|
|
597
596
|
let index = 0;
|
|
598
597
|
while (index < masked.length) {
|
|
@@ -609,7 +608,7 @@ function extractWikilinks(markdown, masked, lineStarts2) {
|
|
|
609
608
|
}
|
|
610
609
|
const parsed = parseWikilinkBody(markdown.slice(start + 2, close2));
|
|
611
610
|
if (parsed) {
|
|
612
|
-
const position = positionAt(
|
|
611
|
+
const position = positionAt(lineStarts, start);
|
|
613
612
|
links.push({
|
|
614
613
|
label: parsed.label ?? parsed.target,
|
|
615
614
|
target: parsed.target,
|
|
@@ -889,12 +888,12 @@ function getLineStarts(value) {
|
|
|
889
888
|
}
|
|
890
889
|
return starts;
|
|
891
890
|
}
|
|
892
|
-
function positionAt(
|
|
891
|
+
function positionAt(lineStarts, index) {
|
|
893
892
|
let low = 0;
|
|
894
|
-
let high =
|
|
893
|
+
let high = lineStarts.length - 1;
|
|
895
894
|
while (low <= high) {
|
|
896
895
|
const middle = Math.floor((low + high) / 2);
|
|
897
|
-
const start =
|
|
896
|
+
const start = lineStarts[middle] ?? 0;
|
|
898
897
|
if (start <= index) {
|
|
899
898
|
low = middle + 1;
|
|
900
899
|
} else {
|
|
@@ -904,532 +903,9 @@ function positionAt(lineStarts2, index) {
|
|
|
904
903
|
const lineIndex = Math.max(0, high);
|
|
905
904
|
return {
|
|
906
905
|
line: lineIndex + 1,
|
|
907
|
-
column: index - (
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// ../core/src/anchors.ts
|
|
912
|
-
function selectorFromRange(markdown, range) {
|
|
913
|
-
const lines = getLines(markdown);
|
|
914
|
-
const selected = lines.slice(range.startLine - 1, range.endLine).join("\n");
|
|
915
|
-
const prefix = lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n");
|
|
916
|
-
const suffix = lines.slice(range.endLine, range.endLine + 3).join("\n");
|
|
917
|
-
return { quote: selected, prefix, suffix };
|
|
918
|
-
}
|
|
919
|
-
function remapAnchor(markdown, anchor) {
|
|
920
|
-
const lines = getLines(markdown);
|
|
921
|
-
const quoteLines = anchor.selector.quote.split(/\r?\n/);
|
|
922
|
-
const candidates = findQuoteCandidates(lines, quoteLines);
|
|
923
|
-
const best = bestCandidate(lines, candidates, anchor.selector);
|
|
924
|
-
if (!best) {
|
|
925
|
-
return {
|
|
926
|
-
...anchor,
|
|
927
|
-
status: "needs_review",
|
|
928
|
-
confidence: 0,
|
|
929
|
-
updatedAt: nowIso()
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
const confidence = scoreContext(lines, best.startLine, best.endLine, anchor.selector);
|
|
933
|
-
return {
|
|
934
|
-
...anchor,
|
|
935
|
-
status: confidence >= 0.65 ? "mapped" : "needs_review",
|
|
936
|
-
confidence,
|
|
937
|
-
range: best,
|
|
938
|
-
updatedAt: nowIso()
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
function remapAnchors(markdown, anchors) {
|
|
942
|
-
return anchors.map((anchor) => remapAnchor(markdown, anchor));
|
|
943
|
-
}
|
|
944
|
-
function findQuoteCandidates(lines, quoteLines) {
|
|
945
|
-
if (quoteLines.length === 0) return [];
|
|
946
|
-
const ranges = [];
|
|
947
|
-
const quoteText = quoteLines.join("\n");
|
|
948
|
-
for (let index = 0; index <= lines.length - quoteLines.length; index += 1) {
|
|
949
|
-
const candidate = lines.slice(index, index + quoteLines.length);
|
|
950
|
-
if (candidate.join("\n") === quoteText) ranges.push({ startLine: index + 1, endLine: index + quoteLines.length });
|
|
951
|
-
}
|
|
952
|
-
return ranges;
|
|
953
|
-
}
|
|
954
|
-
function bestCandidate(lines, candidates, selector) {
|
|
955
|
-
if (candidates.length <= 1) return candidates[0];
|
|
956
|
-
const scored = candidates.map((range) => ({ range, score: scoreContext(lines, range.startLine, range.endLine, selector) })).sort((left, right) => right.score - left.score);
|
|
957
|
-
const best = scored[0];
|
|
958
|
-
if (!best) return void 0;
|
|
959
|
-
const tied = scored.filter((candidate) => candidate.score === best.score);
|
|
960
|
-
return tied.length === 1 ? best.range : void 0;
|
|
961
|
-
}
|
|
962
|
-
function scoreContext(lines, startLine, endLine, selector) {
|
|
963
|
-
let score = 0.7;
|
|
964
|
-
const expectedPrefix = contextPrefix(selector.prefix);
|
|
965
|
-
if (expectedPrefix) {
|
|
966
|
-
const actualPrefix = contextPrefix(lines.slice(Math.max(0, startLine - 4), startLine - 1).join("\n"));
|
|
967
|
-
if (expectedPrefix && actualPrefix.endsWith(expectedPrefix)) score += 0.15;
|
|
968
|
-
}
|
|
969
|
-
const expectedSuffix = contextSuffix(selector.suffix);
|
|
970
|
-
if (expectedSuffix) {
|
|
971
|
-
const actualSuffix = contextSuffix(lines.slice(endLine, endLine + 3).join("\n"));
|
|
972
|
-
if (expectedSuffix && actualSuffix.startsWith(expectedSuffix)) score += 0.15;
|
|
973
|
-
}
|
|
974
|
-
return Math.min(1, score);
|
|
975
|
-
}
|
|
976
|
-
function contextPrefix(value) {
|
|
977
|
-
return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") ?? "";
|
|
978
|
-
}
|
|
979
|
-
function contextSuffix(value) {
|
|
980
|
-
return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") ?? "";
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ../core/src/review-markdown.ts
|
|
984
|
-
var START_MARKER = "<!--mdocs-suggestion";
|
|
985
|
-
var END_MARKER = "<!--/mdocs-suggestion-->";
|
|
986
|
-
function parseReviewMarkdown(reviewMarkdown) {
|
|
987
|
-
const segments = [];
|
|
988
|
-
const suggestions = [];
|
|
989
|
-
let cursor = 0;
|
|
990
|
-
let canonicalOffset = 0;
|
|
991
|
-
while (cursor < reviewMarkdown.length) {
|
|
992
|
-
const markerStart = reviewMarkdown.indexOf(START_MARKER, cursor);
|
|
993
|
-
if (markerStart === -1) {
|
|
994
|
-
appendTextSegment(reviewMarkdown, segments, cursor, reviewMarkdown.length, canonicalOffset);
|
|
995
|
-
break;
|
|
996
|
-
}
|
|
997
|
-
appendTextSegment(reviewMarkdown, segments, cursor, markerStart, canonicalOffset);
|
|
998
|
-
canonicalOffset += markerStart - cursor;
|
|
999
|
-
const startClose = reviewMarkdown.indexOf("-->", markerStart);
|
|
1000
|
-
if (startClose === -1) throw invalidReviewMarkdown("Unclosed mdocs suggestion marker.");
|
|
1001
|
-
const attrs2 = parseMarkerAttributes(reviewMarkdown.slice(markerStart + START_MARKER.length, startClose));
|
|
1002
|
-
if (!attrs2.id) throw invalidReviewMarkdown("Suggestion marker is missing an id.");
|
|
1003
|
-
if (attrs2.kind !== "insert" && attrs2.kind !== "delete" && attrs2.kind !== "replace") {
|
|
1004
|
-
throw invalidReviewMarkdown(`Suggestion ${attrs2.id} has an invalid kind.`);
|
|
1005
|
-
}
|
|
1006
|
-
const bodyStart = startClose + 3;
|
|
1007
|
-
const markerEnd = reviewMarkdown.indexOf(END_MARKER, bodyStart);
|
|
1008
|
-
if (markerEnd === -1) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} is missing its closing marker.`);
|
|
1009
|
-
const body = reviewMarkdown.slice(bodyStart, markerEnd);
|
|
1010
|
-
if (body.includes(START_MARKER)) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} contains a nested suggestion.`);
|
|
1011
|
-
const parsedBody = parseSuggestionBody(attrs2.kind, body, attrs2.id);
|
|
1012
|
-
const token = {
|
|
1013
|
-
id: attrs2.id,
|
|
1014
|
-
kind: attrs2.kind,
|
|
1015
|
-
form: parsedBody.before.includes("\n") || parsedBody.after.includes("\n") ? "block" : "inline",
|
|
1016
|
-
author: actorFromAttributes(attrs2),
|
|
1017
|
-
message: attrs2.message,
|
|
1018
|
-
createdAt: attrs2.createdAt,
|
|
1019
|
-
updatedAt: attrs2.updatedAt,
|
|
1020
|
-
before: parsedBody.before,
|
|
1021
|
-
after: parsedBody.after,
|
|
1022
|
-
markerStart,
|
|
1023
|
-
markerEnd: markerEnd + END_MARKER.length,
|
|
1024
|
-
canonicalStart: canonicalOffset,
|
|
1025
|
-
canonicalEnd: canonicalOffset + parsedBody.before.length,
|
|
1026
|
-
raw: reviewMarkdown.slice(markerStart, markerEnd + END_MARKER.length)
|
|
1027
|
-
};
|
|
1028
|
-
segments.push({ type: "suggestion", token });
|
|
1029
|
-
suggestions.push(token);
|
|
1030
|
-
canonicalOffset += token.before.length;
|
|
1031
|
-
cursor = token.markerEnd;
|
|
1032
|
-
}
|
|
1033
|
-
return {
|
|
1034
|
-
reviewMarkdown,
|
|
1035
|
-
canonicalMarkdown: canonicalFromSegments(segments, statusLookup(void 0)),
|
|
1036
|
-
segments,
|
|
1037
|
-
suggestions
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
function projectCanonicalMarkdown(reviewMarkdown, sidecarStatuses) {
|
|
1041
|
-
return canonicalFromSegments(parseReviewMarkdown(reviewMarkdown).segments, statusLookup(sidecarStatuses));
|
|
1042
|
-
}
|
|
1043
|
-
function deriveSidecarFromReviewMarkdown(sidecar, reviewMarkdown, options) {
|
|
1044
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1045
|
-
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
-
const existingSuggestions = new Map(sidecar.suggestions.map((suggestion) => [suggestion.id, suggestion]));
|
|
1047
|
-
const existingAnchors = new Map(sidecar.anchors.map((anchor) => [anchor.id, anchor]));
|
|
1048
|
-
const anchors = sidecar.anchors.filter((anchor) => anchor.kind !== "suggestion");
|
|
1049
|
-
const suggestions = [];
|
|
1050
|
-
const base = { contentHash: contentHashForText(parsed.canonicalMarkdown), ...options.baseHead ? { head: options.baseHead } : {} };
|
|
1051
|
-
for (const token of parsed.suggestions) {
|
|
1052
|
-
const existing = existingSuggestions.get(token.id);
|
|
1053
|
-
const anchorId = existing?.anchorId ?? stableAnchorIdForSuggestion(token.id);
|
|
1054
|
-
const range = lineRangeForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
|
|
1055
|
-
const selector = selectorForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
|
|
1056
|
-
const anchor = {
|
|
1057
|
-
...existingAnchors.get(anchorId) ?? {},
|
|
1058
|
-
id: anchorId,
|
|
1059
|
-
kind: "suggestion",
|
|
1060
|
-
status: "mapped",
|
|
1061
|
-
selector,
|
|
1062
|
-
range,
|
|
1063
|
-
confidence: 1,
|
|
1064
|
-
updatedAt: now
|
|
1065
|
-
};
|
|
1066
|
-
const author = token.author ?? existing?.author ?? options.author;
|
|
1067
|
-
const message = token.message ?? existing?.message ?? "Suggested edit";
|
|
1068
|
-
const suggestion = {
|
|
1069
|
-
...existing ?? {},
|
|
1070
|
-
id: token.id,
|
|
1071
|
-
anchorId,
|
|
1072
|
-
status: "open",
|
|
1073
|
-
kind: token.kind,
|
|
1074
|
-
author,
|
|
1075
|
-
message,
|
|
1076
|
-
patch: {
|
|
1077
|
-
type: "replace_lines",
|
|
1078
|
-
range,
|
|
1079
|
-
before: token.before,
|
|
1080
|
-
after: token.after
|
|
1081
|
-
},
|
|
1082
|
-
base: existing?.base ?? base,
|
|
1083
|
-
diff: {
|
|
1084
|
-
before: token.before,
|
|
1085
|
-
after: token.after,
|
|
1086
|
-
summary: diffSummary(token.before, token.after)
|
|
1087
|
-
},
|
|
1088
|
-
projectionStatus: "clean",
|
|
1089
|
-
reviewRange: { start: token.markerStart, end: token.markerEnd },
|
|
1090
|
-
createdAt: token.createdAt ?? existing?.createdAt ?? now,
|
|
1091
|
-
updatedAt: token.updatedAt ?? now
|
|
1092
|
-
};
|
|
1093
|
-
anchors.push(anchor);
|
|
1094
|
-
suggestions.push(suggestion);
|
|
1095
|
-
}
|
|
1096
|
-
return {
|
|
1097
|
-
...sidecar,
|
|
1098
|
-
reviewMarkdown,
|
|
1099
|
-
anchors,
|
|
1100
|
-
suggestions,
|
|
1101
|
-
updatedAt: now
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
function createSuggestionMarkup(reviewMarkdown, patch, metadata) {
|
|
1105
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1106
|
-
assertNoDuplicateSuggestionId(parsed, metadata.suggestionId);
|
|
1107
|
-
const [contentStart, contentEnd] = lineRangeOffsets(parsed.canonicalMarkdown, patch.range);
|
|
1108
|
-
const selected = parsed.canonicalMarkdown.slice(contentStart, contentEnd);
|
|
1109
|
-
if (selected !== patch.before) {
|
|
1110
|
-
throw new MdocsError(
|
|
1111
|
-
"conflict",
|
|
1112
|
-
"Suggestion target does not match the review document projection.",
|
|
1113
|
-
"Re-read the document and create the suggestion against the current canonical text."
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
const [canonicalStart, canonicalEnd] = lineRangeReplacementOffsets(parsed.canonicalMarkdown, patch.range);
|
|
1117
|
-
if (selectionTouchesSuggestion(parsed.suggestions, canonicalStart, canonicalEnd)) {
|
|
1118
|
-
throw new MdocsError(
|
|
1119
|
-
"conflict",
|
|
1120
|
-
"Suggestion overlaps an unresolved suggestion.",
|
|
1121
|
-
"Accept or reject the existing suggestion before creating another edit over the same text."
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
const rawStart = rawOffsetForCanonicalOffset(parsed.segments, canonicalStart);
|
|
1125
|
-
const rawEnd = rawOffsetForCanonicalOffset(parsed.segments, canonicalEnd);
|
|
1126
|
-
const before = parsed.canonicalMarkdown.slice(canonicalStart, canonicalEnd);
|
|
1127
|
-
const after = replacementTextForSpan(parsed.canonicalMarkdown, patch, canonicalStart, canonicalEnd);
|
|
1128
|
-
const marker = formatSuggestionMarker(metadata, kindForText(before, after), before, after);
|
|
1129
|
-
const nextReviewMarkdown = `${reviewMarkdown.slice(0, rawStart)}${marker}${reviewMarkdown.slice(rawEnd)}`;
|
|
1130
|
-
const token = parseReviewMarkdown(nextReviewMarkdown).suggestions.find((candidate) => candidate.id === metadata.suggestionId);
|
|
1131
|
-
if (!token) throw invalidReviewMarkdown(`Failed to create suggestion ${metadata.suggestionId}.`);
|
|
1132
|
-
return { reviewMarkdown: nextReviewMarkdown, token };
|
|
1133
|
-
}
|
|
1134
|
-
function projectReviewMarkdown(canonicalMarkdown, suggestions) {
|
|
1135
|
-
let reviewMarkdown = canonicalMarkdown;
|
|
1136
|
-
const open = suggestions.filter((suggestion) => suggestion.status === "open" && suggestion.patch?.type === "replace_lines").slice().sort((left, right) => left.patch.range.startLine - right.patch.range.startLine);
|
|
1137
|
-
for (const suggestion of open) {
|
|
1138
|
-
try {
|
|
1139
|
-
reviewMarkdown = createSuggestionMarkup(reviewMarkdown, suggestion.patch, {
|
|
1140
|
-
suggestionId: suggestion.id,
|
|
1141
|
-
author: suggestion.author,
|
|
1142
|
-
message: suggestion.message,
|
|
1143
|
-
createdAt: suggestion.createdAt,
|
|
1144
|
-
updatedAt: suggestion.updatedAt
|
|
1145
|
-
}).reviewMarkdown;
|
|
1146
|
-
} catch {
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
return reviewMarkdown;
|
|
1150
|
-
}
|
|
1151
|
-
function resolveSuggestionMarkup(reviewMarkdown, suggestionId, resolution) {
|
|
1152
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1153
|
-
const token = parsed.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1154
|
-
if (!token) {
|
|
1155
|
-
throw new MdocsError("not_found", `Unknown suggestion: ${suggestionId}`, "Refresh review state and try again.");
|
|
1156
|
-
}
|
|
1157
|
-
const resolved = resolutionText(token, resolution);
|
|
1158
|
-
return `${reviewMarkdown.slice(0, token.markerStart)}${resolved}${reviewMarkdown.slice(token.markerEnd)}`;
|
|
1159
|
-
}
|
|
1160
|
-
function escapeReviewSuggestionText(value) {
|
|
1161
|
-
return value.replaceAll("\\", "\\\\").replaceAll("{++", "\\{++").replaceAll("++}", "\\++}").replaceAll("{--", "\\{--").replaceAll("--}", "\\--}").replaceAll("{~~", "\\{~~").replaceAll("~>", "\\~>").replaceAll("~~}", "\\~~}");
|
|
1162
|
-
}
|
|
1163
|
-
function lineRangeForCanonicalSpan(markdown, start, end) {
|
|
1164
|
-
assertCanonicalSpan(markdown, start, end);
|
|
1165
|
-
return lineRangeForOffsets(markdown, start, end);
|
|
1166
|
-
}
|
|
1167
|
-
function selectorForCanonicalSpan(markdown, start, end) {
|
|
1168
|
-
assertCanonicalSpan(markdown, start, end);
|
|
1169
|
-
const context = 160;
|
|
1170
|
-
const prefix = markdown.slice(Math.max(0, start - context), start);
|
|
1171
|
-
const suffix = markdown.slice(end, Math.min(markdown.length, end + context));
|
|
1172
|
-
return {
|
|
1173
|
-
quote: markdown.slice(start, end),
|
|
1174
|
-
...prefix ? { prefix } : {},
|
|
1175
|
-
...suffix ? { suffix } : {}
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
function appendTextSegment(source, segments, start, end, canonicalStart) {
|
|
1179
|
-
if (end <= start) return;
|
|
1180
|
-
segments.push({
|
|
1181
|
-
type: "text",
|
|
1182
|
-
text: source.slice(start, end),
|
|
1183
|
-
start,
|
|
1184
|
-
end,
|
|
1185
|
-
canonicalStart,
|
|
1186
|
-
canonicalEnd: canonicalStart + (end - start)
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
function canonicalFromSegments(segments, statuses) {
|
|
1190
|
-
return segments.map((segment) => {
|
|
1191
|
-
if (segment.type === "text") return segment.text;
|
|
1192
|
-
const status = statuses(segment.token.id);
|
|
1193
|
-
if (status === "accepted") return acceptedText(segment.token);
|
|
1194
|
-
return originalText(segment.token);
|
|
1195
|
-
}).join("");
|
|
1196
|
-
}
|
|
1197
|
-
function parseMarkerAttributes(raw) {
|
|
1198
|
-
const attrs2 = {};
|
|
1199
|
-
const pattern = /\s+([a-zA-Z][\w:-]*)="([^"]*)"/g;
|
|
1200
|
-
let match2;
|
|
1201
|
-
while ((match2 = pattern.exec(raw)) !== null) {
|
|
1202
|
-
const key = match2[1];
|
|
1203
|
-
if (key === "id" || key === "kind" || key === "authorId" || key === "authorKind" || key === "authorName" || key === "authorEmail" || key === "message" || key === "createdAt" || key === "updatedAt") {
|
|
1204
|
-
attrs2[key] = decodeAttribute(match2[2] ?? "");
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return attrs2;
|
|
1208
|
-
}
|
|
1209
|
-
function actorFromAttributes(attrs2) {
|
|
1210
|
-
if (!attrs2.authorId || !attrs2.authorName) return void 0;
|
|
1211
|
-
const kind = attrs2.authorKind === "agent" || attrs2.authorKind === "system" || attrs2.authorKind === "human" ? attrs2.authorKind : "human";
|
|
1212
|
-
return {
|
|
1213
|
-
id: attrs2.authorId,
|
|
1214
|
-
kind,
|
|
1215
|
-
name: attrs2.authorName,
|
|
1216
|
-
...attrs2.authorEmail ? { email: attrs2.authorEmail } : {}
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
function parseSuggestionBody(kind, body, id) {
|
|
1220
|
-
if (kind === "insert") {
|
|
1221
|
-
return { before: "", after: parseWrappedBody(body, "{++", "++}", id) };
|
|
1222
|
-
}
|
|
1223
|
-
if (kind === "delete") {
|
|
1224
|
-
return { before: parseWrappedBody(body, "{--", "--}", id), after: "" };
|
|
1225
|
-
}
|
|
1226
|
-
if (!body.startsWith("{~~") || !body.endsWith("~~}")) {
|
|
1227
|
-
throw invalidReviewMarkdown(`Replacement suggestion ${id} has invalid CriticMarkup payload.`);
|
|
1228
|
-
}
|
|
1229
|
-
const inner = body.slice(3, -3);
|
|
1230
|
-
const separator = findUnescaped2(inner, "~>");
|
|
1231
|
-
if (separator === -1) throw invalidReviewMarkdown(`Replacement suggestion ${id} is missing a separator.`);
|
|
1232
|
-
return {
|
|
1233
|
-
before: unescapeReviewSuggestionText(inner.slice(0, separator)),
|
|
1234
|
-
after: unescapeReviewSuggestionText(inner.slice(separator + 2))
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
function parseWrappedBody(body, open, close2, id) {
|
|
1238
|
-
if (!body.startsWith(open) || !body.endsWith(close2) || isEscapedAt(body, body.length - close2.length)) {
|
|
1239
|
-
throw invalidReviewMarkdown(`Suggestion ${id} has invalid CriticMarkup payload.`);
|
|
1240
|
-
}
|
|
1241
|
-
return unescapeReviewSuggestionText(body.slice(open.length, -close2.length));
|
|
1242
|
-
}
|
|
1243
|
-
function formatSuggestionMarker(metadata, kind, before, after) {
|
|
1244
|
-
const attrs2 = [
|
|
1245
|
-
["id", metadata.suggestionId],
|
|
1246
|
-
["kind", kind]
|
|
1247
|
-
];
|
|
1248
|
-
if (metadata.author) {
|
|
1249
|
-
attrs2.push(["authorId", metadata.author.id]);
|
|
1250
|
-
attrs2.push(["authorKind", metadata.author.kind]);
|
|
1251
|
-
attrs2.push(["authorName", metadata.author.name]);
|
|
1252
|
-
if (metadata.author.email) attrs2.push(["authorEmail", metadata.author.email]);
|
|
1253
|
-
}
|
|
1254
|
-
if (metadata.message) attrs2.push(["message", metadata.message]);
|
|
1255
|
-
if (metadata.createdAt) attrs2.push(["createdAt", metadata.createdAt]);
|
|
1256
|
-
if (metadata.updatedAt) attrs2.push(["updatedAt", metadata.updatedAt]);
|
|
1257
|
-
const attrText = attrs2.map(([key, value]) => `${key}="${encodeAttribute(value)}"`).join(" ");
|
|
1258
|
-
const body = kind === "insert" ? `{++${escapeReviewSuggestionText(after)}++}` : kind === "delete" ? `{--${escapeReviewSuggestionText(before)}--}` : `{~~${escapeReviewSuggestionText(before)}~>${escapeReviewSuggestionText(after)}~~}`;
|
|
1259
|
-
return `${START_MARKER} ${attrText} -->${body}${END_MARKER}`;
|
|
1260
|
-
}
|
|
1261
|
-
function stableAnchorIdForSuggestion(suggestionId) {
|
|
1262
|
-
return `anchor_${suggestionId.replace(/[^a-zA-Z0-9_:-]/g, "_")}`;
|
|
1263
|
-
}
|
|
1264
|
-
function diffSummary(before, after) {
|
|
1265
|
-
if (!before && after) return "Inserted text";
|
|
1266
|
-
if (before && !after) return "Deleted text";
|
|
1267
|
-
return "Replaced text";
|
|
1268
|
-
}
|
|
1269
|
-
function unescapeReviewSuggestionText(value) {
|
|
1270
|
-
let result = "";
|
|
1271
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1272
|
-
if (value[index] !== "\\") {
|
|
1273
|
-
result += value[index];
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
const next = value[index + 1];
|
|
1277
|
-
if (next === void 0) {
|
|
1278
|
-
result += "\\";
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
result += next;
|
|
1282
|
-
index += 1;
|
|
1283
|
-
}
|
|
1284
|
-
return result;
|
|
1285
|
-
}
|
|
1286
|
-
function findUnescaped2(value, needle, from = 0) {
|
|
1287
|
-
let index = value.indexOf(needle, from);
|
|
1288
|
-
while (index !== -1) {
|
|
1289
|
-
if (!isEscapedAt(value, index)) return index;
|
|
1290
|
-
index = value.indexOf(needle, index + needle.length);
|
|
1291
|
-
}
|
|
1292
|
-
return -1;
|
|
1293
|
-
}
|
|
1294
|
-
function isEscapedAt(value, index) {
|
|
1295
|
-
let slashes = 0;
|
|
1296
|
-
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
|
|
1297
|
-
return slashes % 2 === 1;
|
|
1298
|
-
}
|
|
1299
|
-
function statusLookup(statuses) {
|
|
1300
|
-
if (!statuses) return () => "open";
|
|
1301
|
-
if (isStatusMap(statuses)) return (suggestionId) => statuses.get(suggestionId) ?? "open";
|
|
1302
|
-
if (Array.isArray(statuses)) {
|
|
1303
|
-
const map = /* @__PURE__ */ new Map();
|
|
1304
|
-
for (const item of statuses) {
|
|
1305
|
-
const id = item.id ?? item.suggestionId;
|
|
1306
|
-
if (id && item.status) map.set(id, item.status);
|
|
1307
|
-
}
|
|
1308
|
-
return (suggestionId) => map.get(suggestionId) ?? "open";
|
|
1309
|
-
}
|
|
1310
|
-
const record = statuses;
|
|
1311
|
-
return (suggestionId) => record[suggestionId] ?? "open";
|
|
1312
|
-
}
|
|
1313
|
-
function isStatusMap(value) {
|
|
1314
|
-
return typeof value.get === "function";
|
|
1315
|
-
}
|
|
1316
|
-
function acceptedText(token) {
|
|
1317
|
-
return token.kind === "delete" ? "" : token.after;
|
|
1318
|
-
}
|
|
1319
|
-
function originalText(token) {
|
|
1320
|
-
return token.kind === "insert" ? "" : token.before;
|
|
1321
|
-
}
|
|
1322
|
-
function resolutionText(token, resolution) {
|
|
1323
|
-
return resolution === "accept" ? acceptedText(token) : originalText(token);
|
|
1324
|
-
}
|
|
1325
|
-
function kindForText(before, after) {
|
|
1326
|
-
if (before.length === 0) return "insert";
|
|
1327
|
-
if (after.length === 0) return "delete";
|
|
1328
|
-
return "replace";
|
|
1329
|
-
}
|
|
1330
|
-
function assertNoDuplicateSuggestionId(parsed, suggestionId) {
|
|
1331
|
-
if (parsed.suggestions.some((suggestion) => suggestion.id === suggestionId)) {
|
|
1332
|
-
throw new MdocsError("validation_error", `Duplicate suggestion id: ${suggestionId}`, "Suggestion ids must be stable and unique per document.");
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function assertCanonicalSpan(markdown, start, end) {
|
|
1336
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > markdown.length) {
|
|
1337
|
-
throw new MdocsError("invalid_range", `Invalid canonical span ${start}:${end}.`, "Re-read the document and try again.");
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
function selectionTouchesSuggestion(tokens, start, end) {
|
|
1341
|
-
return tokens.some((token) => {
|
|
1342
|
-
if (token.canonicalStart === token.canonicalEnd) return start <= token.canonicalStart && end >= token.canonicalEnd;
|
|
1343
|
-
return start < token.canonicalEnd && end > token.canonicalStart;
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
function rawOffsetForCanonicalOffset(segments, offset) {
|
|
1347
|
-
for (const segment of segments) {
|
|
1348
|
-
if (segment.type === "text") {
|
|
1349
|
-
if (offset >= segment.canonicalStart && offset <= segment.canonicalEnd) {
|
|
1350
|
-
return segment.start + (offset - segment.canonicalStart);
|
|
1351
|
-
}
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
const token = segment.token;
|
|
1355
|
-
if (offset > token.canonicalStart && offset < token.canonicalEnd) {
|
|
1356
|
-
throw new MdocsError(
|
|
1357
|
-
"conflict",
|
|
1358
|
-
"Selection falls inside an unresolved suggestion.",
|
|
1359
|
-
"Accept or reject the existing suggestion before editing inside it."
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
if (offset === segmentsEndCanonicalOffset(segments)) return segmentsEndRawOffset(segments);
|
|
1364
|
-
throw new MdocsError("invalid_range", "Could not map canonical offset into review Markdown.", "Re-read the document and try again.");
|
|
1365
|
-
}
|
|
1366
|
-
function segmentsEndCanonicalOffset(segments) {
|
|
1367
|
-
const last = segments[segments.length - 1];
|
|
1368
|
-
if (!last) return 0;
|
|
1369
|
-
return last.type === "text" ? last.canonicalEnd : last.token.canonicalEnd;
|
|
1370
|
-
}
|
|
1371
|
-
function segmentsEndRawOffset(segments) {
|
|
1372
|
-
const last = segments[segments.length - 1];
|
|
1373
|
-
if (!last) return 0;
|
|
1374
|
-
return last.type === "text" ? last.end : last.token.markerEnd;
|
|
1375
|
-
}
|
|
1376
|
-
function lineRangeOffsets(markdown, range) {
|
|
1377
|
-
const lines = getLines(markdown);
|
|
1378
|
-
if (range.startLine < 1 || range.endLine < range.startLine || range.endLine > lines.length) {
|
|
1379
|
-
throw new MdocsError("invalid_range", `Invalid line range ${range.startLine}:${range.endLine}.`, "Re-read the document and try again.");
|
|
1380
|
-
}
|
|
1381
|
-
let start = 0;
|
|
1382
|
-
for (let index = 0; index < range.startLine - 1; index += 1) start += lines[index].length + 1;
|
|
1383
|
-
let end = start;
|
|
1384
|
-
for (let index = range.startLine - 1; index < range.endLine; index += 1) {
|
|
1385
|
-
if (index > range.startLine - 1) end += 1;
|
|
1386
|
-
end += lines[index].length;
|
|
1387
|
-
}
|
|
1388
|
-
return [start, end];
|
|
1389
|
-
}
|
|
1390
|
-
function lineRangeReplacementOffsets(markdown, range) {
|
|
1391
|
-
const [start, contentEnd] = lineRangeOffsets(markdown, range);
|
|
1392
|
-
if (contentEnd < markdown.length && markdown[contentEnd] === "\n") return [start, contentEnd + 1];
|
|
1393
|
-
return [start, contentEnd];
|
|
1394
|
-
}
|
|
1395
|
-
function replacementTextForSpan(markdown, patch, start, end) {
|
|
1396
|
-
const applied = replaceLineRange(markdown, patch.range, patch.after);
|
|
1397
|
-
const suffixLength = markdown.length - end;
|
|
1398
|
-
const replacementEnd = applied.length - suffixLength;
|
|
1399
|
-
if (replacementEnd < start) return "";
|
|
1400
|
-
return applied.slice(start, replacementEnd);
|
|
1401
|
-
}
|
|
1402
|
-
function lineRangeForOffsets(markdown, start, end) {
|
|
1403
|
-
const starts = lineStarts(markdown);
|
|
1404
|
-
return {
|
|
1405
|
-
startLine: lineNumberAt(starts, start),
|
|
1406
|
-
endLine: lineNumberAt(starts, Math.max(start, end - 1))
|
|
906
|
+
column: index - (lineStarts[lineIndex] ?? 0) + 1
|
|
1407
907
|
};
|
|
1408
908
|
}
|
|
1409
|
-
function lineStarts(markdown) {
|
|
1410
|
-
const starts = [0];
|
|
1411
|
-
for (let index = 0; index < markdown.length; index += 1) {
|
|
1412
|
-
if (markdown[index] === "\n") starts.push(index + 1);
|
|
1413
|
-
}
|
|
1414
|
-
return starts;
|
|
1415
|
-
}
|
|
1416
|
-
function lineNumberAt(starts, offset) {
|
|
1417
|
-
let line = 1;
|
|
1418
|
-
for (let index = 0; index < starts.length; index += 1) {
|
|
1419
|
-
if (starts[index] <= offset) line = index + 1;
|
|
1420
|
-
else break;
|
|
1421
|
-
}
|
|
1422
|
-
return line;
|
|
1423
|
-
}
|
|
1424
|
-
function encodeAttribute(value) {
|
|
1425
|
-
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
1426
|
-
}
|
|
1427
|
-
function decodeAttribute(value) {
|
|
1428
|
-
return value.replaceAll(""", '"').replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
1429
|
-
}
|
|
1430
|
-
function invalidReviewMarkdown(message) {
|
|
1431
|
-
return new MdocsError("validation_error", `Invalid review Markdown: ${message}`, "Repair or remove the malformed suggestion markup.");
|
|
1432
|
-
}
|
|
1433
909
|
|
|
1434
910
|
// ../core/src/graph.ts
|
|
1435
911
|
function resolveMarkdownLinks(links, currentDocument, documents) {
|
|
@@ -1609,23 +1085,16 @@ function safeDecode(value) {
|
|
|
1609
1085
|
}
|
|
1610
1086
|
}
|
|
1611
1087
|
|
|
1612
|
-
// ../core/src/
|
|
1613
|
-
|
|
1614
|
-
return {
|
|
1615
|
-
id: "actor_local",
|
|
1616
|
-
kind: "human",
|
|
1617
|
-
name
|
|
1618
|
-
};
|
|
1619
|
-
}
|
|
1088
|
+
// ../core/src/workspace-store.ts
|
|
1089
|
+
var cacheLookupSignatures = /* @__PURE__ */ new WeakMap();
|
|
1620
1090
|
async function initWorkspace(io, workspaceId = createId("workspace")) {
|
|
1621
1091
|
await io.mkdir(".mdocs");
|
|
1622
|
-
await io.mkdir(SIDECAR_DIR);
|
|
1623
1092
|
await io.mkdir(CHECKPOINT_DIR);
|
|
1624
1093
|
const existing = await readManifestIfPresent(io);
|
|
1625
1094
|
if (existing) return existing;
|
|
1626
1095
|
const now = nowIso();
|
|
1627
1096
|
const manifest = {
|
|
1628
|
-
schemaVersion:
|
|
1097
|
+
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
1629
1098
|
workspaceId,
|
|
1630
1099
|
createdAt: now,
|
|
1631
1100
|
updatedAt: now,
|
|
@@ -1634,315 +1103,169 @@ async function initWorkspace(io, workspaceId = createId("workspace")) {
|
|
|
1634
1103
|
await writeManifest(io, manifest);
|
|
1635
1104
|
return manifest;
|
|
1636
1105
|
}
|
|
1637
|
-
async function indexWorkspace(io) {
|
|
1638
|
-
|
|
1639
|
-
const markdownFiles = await io.listMarkdownFiles();
|
|
1640
|
-
const existingByPath = new Map(manifest.docs.map((doc) => [doc.path, doc]));
|
|
1641
|
-
const markdownFileSet = new Set(markdownFiles);
|
|
1642
|
-
const unmatchedExisting = manifest.docs.filter((doc) => !markdownFileSet.has(doc.path));
|
|
1643
|
-
const claimedRenamedDocIds = /* @__PURE__ */ new Set();
|
|
1644
|
-
const now = nowIso();
|
|
1645
|
-
const docs = [];
|
|
1646
|
-
const markdownByPath = /* @__PURE__ */ new Map();
|
|
1647
|
-
const newPathCountByHash = /* @__PURE__ */ new Map();
|
|
1648
|
-
for (const path of markdownFiles) {
|
|
1649
|
-
const markdown = await io.readText(path);
|
|
1650
|
-
assertCleanMarkdown(markdown);
|
|
1651
|
-
const contentHash = contentHashForText(markdown);
|
|
1652
|
-
markdownByPath.set(path, { contentHash, markdown });
|
|
1653
|
-
newPathCountByHash.set(contentHash, (newPathCountByHash.get(contentHash) ?? 0) + 1);
|
|
1654
|
-
}
|
|
1655
|
-
for (const path of markdownFiles) {
|
|
1656
|
-
const { contentHash, markdown } = markdownByPath.get(path);
|
|
1657
|
-
const existing = existingByPath.get(path) ?? await findRenamedDocumentEntry(
|
|
1658
|
-
io,
|
|
1659
|
-
unmatchedExisting.filter((doc) => !claimedRenamedDocIds.has(doc.docId)),
|
|
1660
|
-
markdown,
|
|
1661
|
-
contentHash,
|
|
1662
|
-
newPathCountByHash.get(contentHash) ?? 0
|
|
1663
|
-
);
|
|
1664
|
-
if (existing && existing.path !== path) claimedRenamedDocIds.add(existing.docId);
|
|
1665
|
-
const docId = existing?.docId ?? createDocId();
|
|
1666
|
-
const title = titleFromMarkdown(path, markdown);
|
|
1667
|
-
const entry = {
|
|
1668
|
-
docId,
|
|
1669
|
-
path,
|
|
1670
|
-
title,
|
|
1671
|
-
contentHash,
|
|
1672
|
-
currentSha: existing?.currentSha,
|
|
1673
|
-
updatedAt: now
|
|
1674
|
-
};
|
|
1675
|
-
docs.push(entry);
|
|
1676
|
-
await ensureDocumentSidecar(io, entry, markdown);
|
|
1677
|
-
}
|
|
1678
|
-
const next = {
|
|
1679
|
-
...manifest,
|
|
1680
|
-
docs,
|
|
1681
|
-
updatedAt: now
|
|
1682
|
-
};
|
|
1683
|
-
await writeManifest(io, next);
|
|
1684
|
-
return next;
|
|
1106
|
+
async function indexWorkspace(io, options) {
|
|
1107
|
+
return (await computeWorkspaceIndex(io, options)).manifest;
|
|
1685
1108
|
}
|
|
1686
|
-
async function getWorkspaceMap(io) {
|
|
1687
|
-
const manifest = await
|
|
1688
|
-
const graph =
|
|
1109
|
+
async function getWorkspaceMap(io, options) {
|
|
1110
|
+
const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
|
|
1111
|
+
const graph = buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
|
|
1689
1112
|
const graphNodesByDocId = new Map(graph.nodes.map((node) => [node.docId, node]));
|
|
1690
|
-
|
|
1691
|
-
manifest.
|
|
1692
|
-
|
|
1113
|
+
return {
|
|
1114
|
+
workspaceId: manifest.workspaceId,
|
|
1115
|
+
schemaVersion: manifest.schemaVersion,
|
|
1116
|
+
docs: manifest.docs.map((entry) => {
|
|
1693
1117
|
const graphNode = graphNodesByDocId.get(entry.docId);
|
|
1694
1118
|
return {
|
|
1695
1119
|
docId: entry.docId,
|
|
1696
1120
|
path: entry.path,
|
|
1697
1121
|
title: entry.title,
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1122
|
+
openComments: 0,
|
|
1123
|
+
openSuggestions: 0,
|
|
1124
|
+
anchorsNeedingReview: 0,
|
|
1702
1125
|
outgoingLinks: graphNode?.outgoingLinks ?? 0,
|
|
1703
1126
|
incomingLinks: graphNode?.incomingLinks ?? 0,
|
|
1704
1127
|
unresolvedLinks: graphNode?.unresolvedLinks ?? 0,
|
|
1705
1128
|
externalLinks: graphNode?.externalLinks ?? 0
|
|
1706
1129
|
};
|
|
1707
1130
|
})
|
|
1708
|
-
);
|
|
1709
|
-
return {
|
|
1710
|
-
workspaceId: manifest.workspaceId,
|
|
1711
|
-
schemaVersion: manifest.schemaVersion,
|
|
1712
|
-
docs
|
|
1713
1131
|
};
|
|
1714
1132
|
}
|
|
1715
|
-
async function getWorkspaceGraph(io) {
|
|
1716
|
-
|
|
1133
|
+
async function getWorkspaceGraph(io, options) {
|
|
1134
|
+
const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
|
|
1135
|
+
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
|
|
1717
1136
|
}
|
|
1718
|
-
async function getDocumentState(io, pathOrDocId) {
|
|
1719
|
-
const manifest = await
|
|
1137
|
+
async function getDocumentState(io, pathOrDocId, options) {
|
|
1138
|
+
const { manifest, graphDocuments, markdownByPath } = await computeWorkspaceIndex(io, options);
|
|
1720
1139
|
const entry = findDoc(manifest, pathOrDocId);
|
|
1721
|
-
const markdown = await io.readText(entry.path);
|
|
1140
|
+
const markdown = markdownByPath.get(entry.path) ?? await io.readText(entry.path);
|
|
1722
1141
|
assertCleanMarkdown(markdown);
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
const nextSidecar = {
|
|
1726
|
-
...sidecar,
|
|
1727
|
-
anchors: remappedAnchors,
|
|
1728
|
-
updatedAt: nowIso()
|
|
1729
|
-
};
|
|
1730
|
-
await writeSidecar(io, nextSidecar);
|
|
1142
|
+
const source = graphDocuments.find((document) => document.docId === entry.docId);
|
|
1143
|
+
const links = source?.links ?? resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1731
1144
|
return {
|
|
1732
1145
|
docId: entry.docId,
|
|
1733
1146
|
path: entry.path,
|
|
1734
1147
|
title: entry.title,
|
|
1735
1148
|
markdown,
|
|
1736
|
-
reviewMarkdown: nextSidecar.reviewMarkdown ?? markdown,
|
|
1737
|
-
sidecar: nextSidecar,
|
|
1738
|
-
anchors: remappedAnchors.map((anchor) => ({
|
|
1739
|
-
id: anchor.id,
|
|
1740
|
-
kind: anchor.kind,
|
|
1741
|
-
status: anchor.status,
|
|
1742
|
-
range: anchor.range,
|
|
1743
|
-
quote: anchor.selector.quote,
|
|
1744
|
-
confidence: anchor.confidence
|
|
1745
|
-
})),
|
|
1746
1149
|
images: extractMarkdownImages(markdown, entry.path),
|
|
1747
|
-
links
|
|
1748
|
-
openComments:
|
|
1749
|
-
openSuggestions:
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
async function workspaceGraphForManifest(io, manifest) {
|
|
1753
|
-
const documents = await Promise.all(
|
|
1754
|
-
manifest.docs.map(async (entry) => {
|
|
1755
|
-
const [markdown, sidecar] = await Promise.all([io.readText(entry.path), readSidecar(io, entry.docId)]);
|
|
1756
|
-
const links = resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1757
|
-
return {
|
|
1758
|
-
docId: entry.docId,
|
|
1759
|
-
path: entry.path,
|
|
1760
|
-
title: entry.title,
|
|
1761
|
-
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
1762
|
-
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
1763
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1764
|
-
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1765
|
-
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1766
|
-
links
|
|
1767
|
-
};
|
|
1768
|
-
})
|
|
1769
|
-
);
|
|
1770
|
-
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, documents);
|
|
1771
|
-
}
|
|
1772
|
-
async function addComment(io, pathOrDocId, range, body, author = defaultActor()) {
|
|
1773
|
-
const state = await getDocumentState(io, pathOrDocId);
|
|
1774
|
-
assertLineRangeWithin(state.markdown, range);
|
|
1775
|
-
const now = nowIso();
|
|
1776
|
-
const anchor = {
|
|
1777
|
-
id: createId("anchor"),
|
|
1778
|
-
kind: "comment",
|
|
1779
|
-
status: "mapped",
|
|
1780
|
-
selector: selectorFromRange(state.markdown, range),
|
|
1781
|
-
range,
|
|
1782
|
-
confidence: 1,
|
|
1783
|
-
updatedAt: now
|
|
1784
|
-
};
|
|
1785
|
-
const comment2 = {
|
|
1786
|
-
id: createId("comment"),
|
|
1787
|
-
anchorId: anchor.id,
|
|
1788
|
-
status: "open",
|
|
1789
|
-
author,
|
|
1790
|
-
body,
|
|
1791
|
-
replies: [],
|
|
1792
|
-
createdAt: now,
|
|
1793
|
-
updatedAt: now
|
|
1150
|
+
links,
|
|
1151
|
+
openComments: 0,
|
|
1152
|
+
openSuggestions: 0,
|
|
1153
|
+
anchorsNeedingReview: 0
|
|
1794
1154
|
};
|
|
1795
|
-
await writeSidecar(io, {
|
|
1796
|
-
...state.sidecar,
|
|
1797
|
-
anchors: [...state.sidecar.anchors, anchor],
|
|
1798
|
-
comments: [...state.sidecar.comments, comment2],
|
|
1799
|
-
updatedAt: now
|
|
1800
|
-
});
|
|
1801
|
-
return comment2;
|
|
1802
1155
|
}
|
|
1803
|
-
async function
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
assertCleanMarkdown(replacement);
|
|
1156
|
+
async function computeWorkspaceIndex(io, options) {
|
|
1157
|
+
const baseline = await initWorkspace(io);
|
|
1158
|
+
const cache = options?.cache;
|
|
1159
|
+
const markdownFiles = await io.listMarkdownFiles();
|
|
1160
|
+
const existingByPath = new Map(baseline.docs.map((doc) => [doc.path, doc]));
|
|
1809
1161
|
const now = nowIso();
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
author,
|
|
1844
|
-
message,
|
|
1845
|
-
createdAt: now,
|
|
1846
|
-
updatedAt: now
|
|
1847
|
-
});
|
|
1848
|
-
const nextSidecar = deriveSidecarFromReviewMarkdown(
|
|
1849
|
-
{ ...state.sidecar, reviewMarkdown: review.reviewMarkdown, anchors: [...state.sidecar.anchors, anchor] },
|
|
1850
|
-
review.reviewMarkdown,
|
|
1851
|
-
{ author, baseHead: entry?.currentSha, now }
|
|
1852
|
-
);
|
|
1853
|
-
await writeSidecar(io, nextSidecar);
|
|
1854
|
-
const enriched = nextSidecar.suggestions.find((candidate) => candidate.id === suggestion.id);
|
|
1855
|
-
if (!enriched) throw new MdocsError("validation_error", `Could not derive suggestion ${suggestion.id}.`, "Refresh review state and try again.");
|
|
1856
|
-
return enriched;
|
|
1857
|
-
}
|
|
1858
|
-
async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
|
|
1859
|
-
const manifest = await indexWorkspace(io);
|
|
1860
|
-
for (const entry of manifest.docs) {
|
|
1861
|
-
const markdown = await io.readText(entry.path);
|
|
1862
|
-
const sidecar = await readSidecar(io, entry.docId);
|
|
1863
|
-
const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1864
|
-
if (!suggestion) continue;
|
|
1865
|
-
const now = nowIso();
|
|
1866
|
-
const reviewMarkdown = sidecar.reviewMarkdown ?? markdown;
|
|
1867
|
-
const resolvedReviewMarkdown = resolveSuggestionMarkup(reviewMarkdown, suggestionId, "accept");
|
|
1868
|
-
const nextMarkdown = projectCanonicalMarkdown(resolvedReviewMarkdown);
|
|
1869
|
-
assertCleanMarkdown(nextMarkdown);
|
|
1870
|
-
const nextSidecar = deriveSidecarFromReviewMarkdown(
|
|
1871
|
-
{ ...sidecar, reviewMarkdown: resolvedReviewMarkdown },
|
|
1872
|
-
resolvedReviewMarkdown,
|
|
1873
|
-
{ author: suggestion.author, baseHead: entry.currentSha, now }
|
|
1874
|
-
);
|
|
1875
|
-
await io.writeText(entry.path, nextMarkdown);
|
|
1876
|
-
await writeSidecar(io, nextSidecar);
|
|
1877
|
-
return resolvedSuggestion(suggestion, "accepted", actor, now);
|
|
1162
|
+
const drafts = [];
|
|
1163
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1164
|
+
for (const path of markdownFiles) {
|
|
1165
|
+
seenPaths.add(path);
|
|
1166
|
+
const existing = existingByPath.get(path);
|
|
1167
|
+
const stat4 = io.statFile ? await io.statFile(path).catch(() => void 0) : void 0;
|
|
1168
|
+
const cached = cache?.get(path);
|
|
1169
|
+
const statUnchanged = Boolean(stat4 && cached && cached.mtimeMs === stat4.mtimeMs && cached.size === stat4.size);
|
|
1170
|
+
if (statUnchanged && cached) {
|
|
1171
|
+
drafts.push({
|
|
1172
|
+
entry: manifestEntry(cached.docId, path, cached.title, cached.contentHash, existing, now, stat4),
|
|
1173
|
+
markdown: cached.markdown,
|
|
1174
|
+
reusedFromCache: true
|
|
1175
|
+
});
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
let markdown;
|
|
1179
|
+
try {
|
|
1180
|
+
markdown = await io.readText(path);
|
|
1181
|
+
assertCleanMarkdown(markdown);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
emitIndexDiagnostic(path, error);
|
|
1184
|
+
if (existing) drafts.push({ entry: { ...existing }, markdown: cached?.markdown ?? "", reusedFromCache: true });
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const contentHash = contentHashForText(markdown);
|
|
1188
|
+
const title = titleFromMarkdown(path, markdown);
|
|
1189
|
+
const docId = existing?.docId ?? mintDocId(options?.rootId, path);
|
|
1190
|
+
drafts.push({
|
|
1191
|
+
entry: manifestEntry(docId, path, title, contentHash, existing, now, stat4),
|
|
1192
|
+
markdown,
|
|
1193
|
+
reusedFromCache: false
|
|
1194
|
+
});
|
|
1878
1195
|
}
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1196
|
+
const docs = drafts.map((draft) => draft.entry);
|
|
1197
|
+
const lookup = docs.map((entry) => ({ docId: entry.docId, path: entry.path, title: entry.title }));
|
|
1198
|
+
const lookupSignature = JSON.stringify(lookup);
|
|
1199
|
+
const reuseLinks = cache ? cacheLookupSignatures.get(cache) === lookupSignature : false;
|
|
1200
|
+
const graphDocuments = [];
|
|
1201
|
+
const markdownByPath = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (const draft of drafts) {
|
|
1203
|
+
const { entry, markdown, reusedFromCache } = draft;
|
|
1204
|
+
markdownByPath.set(entry.path, markdown);
|
|
1205
|
+
const cached = cache?.get(entry.path);
|
|
1206
|
+
const links = reusedFromCache && reuseLinks && cached?.links && cached.docId === entry.docId ? cached.links : resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, lookup);
|
|
1207
|
+
graphDocuments.push({
|
|
1208
|
+
docId: entry.docId,
|
|
1209
|
+
path: entry.path,
|
|
1210
|
+
title: entry.title,
|
|
1211
|
+
openComments: 0,
|
|
1212
|
+
openSuggestions: 0,
|
|
1213
|
+
anchorsNeedingReview: 0,
|
|
1214
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1215
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1216
|
+
links
|
|
1893
1217
|
});
|
|
1894
|
-
|
|
1895
|
-
|
|
1218
|
+
if (cache && entry.mtimeMs !== void 0 && entry.size !== void 0) {
|
|
1219
|
+
cache.set(entry.path, {
|
|
1220
|
+
mtimeMs: entry.mtimeMs,
|
|
1221
|
+
size: entry.size,
|
|
1222
|
+
contentHash: entry.contentHash ?? contentHashForText(markdown),
|
|
1223
|
+
markdown,
|
|
1224
|
+
title: entry.title,
|
|
1225
|
+
docId: entry.docId,
|
|
1226
|
+
links
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (cache) {
|
|
1231
|
+
cacheLookupSignatures.set(cache, lookupSignature);
|
|
1232
|
+
for (const key of [...cache.keys()]) {
|
|
1233
|
+
if (!seenPaths.has(key)) cache.delete(key);
|
|
1234
|
+
}
|
|
1896
1235
|
}
|
|
1897
|
-
|
|
1236
|
+
const nextManifest = { ...baseline, docs, updatedAt: baseline.updatedAt };
|
|
1237
|
+
if (stableManifestKey(baseline) === stableManifestKey(nextManifest)) {
|
|
1238
|
+
return { manifest: baseline, graphDocuments, markdownByPath };
|
|
1239
|
+
}
|
|
1240
|
+
const written = { ...nextManifest, updatedAt: now };
|
|
1241
|
+
await writeManifest(io, written);
|
|
1242
|
+
return { manifest: written, graphDocuments, markdownByPath };
|
|
1898
1243
|
}
|
|
1899
|
-
function
|
|
1244
|
+
function manifestEntry(docId, path, title, contentHash, existing, now, stat4) {
|
|
1900
1245
|
return {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1246
|
+
docId,
|
|
1247
|
+
path,
|
|
1248
|
+
title,
|
|
1249
|
+
contentHash,
|
|
1250
|
+
currentSha: existing?.currentSha,
|
|
1251
|
+
// Preserve the prior timestamp when the content is unchanged so the manifest
|
|
1252
|
+
// stays byte-identical; only a real content change advances it.
|
|
1253
|
+
updatedAt: existing && existing.contentHash === contentHash ? existing.updatedAt : now,
|
|
1254
|
+
...stat4 ? { mtimeMs: stat4.mtimeMs, size: stat4.size } : {}
|
|
1906
1255
|
};
|
|
1907
1256
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
for (const entry of manifest.docs) {
|
|
1911
|
-
const sidecar = await readSidecar(io, entry.docId);
|
|
1912
|
-
const comment2 = sidecar.comments.find((candidate) => candidate.id === commentId);
|
|
1913
|
-
if (!comment2) continue;
|
|
1914
|
-
const now = nowIso();
|
|
1915
|
-
const updated = {
|
|
1916
|
-
...comment2,
|
|
1917
|
-
status: "resolved",
|
|
1918
|
-
updatedAt: now,
|
|
1919
|
-
resolvedAt: now,
|
|
1920
|
-
resolvedBy: actor
|
|
1921
|
-
};
|
|
1922
|
-
await writeSidecar(io, {
|
|
1923
|
-
...sidecar,
|
|
1924
|
-
comments: sidecar.comments.map((candidate) => candidate.id === commentId ? updated : candidate),
|
|
1925
|
-
updatedAt: now
|
|
1926
|
-
});
|
|
1927
|
-
return updated;
|
|
1928
|
-
}
|
|
1929
|
-
throw new MdocsError("not_found", `Unknown comment: ${commentId}`, "List comment ids with the comments command.");
|
|
1257
|
+
function mintDocId(rootId, path) {
|
|
1258
|
+
return rootId ? deterministicDocId(rootId, path) : createDocId();
|
|
1930
1259
|
}
|
|
1931
|
-
function
|
|
1932
|
-
return {
|
|
1933
|
-
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
1934
|
-
docId: entry.docId,
|
|
1935
|
-
path: entry.path,
|
|
1936
|
-
title: entry.title,
|
|
1937
|
-
anchors: [],
|
|
1938
|
-
comments: [],
|
|
1939
|
-
suggestions: [],
|
|
1940
|
-
changeSets: [],
|
|
1941
|
-
updatedAt: entry.updatedAt
|
|
1942
|
-
};
|
|
1260
|
+
function stableManifestKey(manifest) {
|
|
1261
|
+
return JSON.stringify({ ...manifest, updatedAt: "" });
|
|
1943
1262
|
}
|
|
1944
|
-
function
|
|
1945
|
-
|
|
1263
|
+
function emitIndexDiagnostic(path, error) {
|
|
1264
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1265
|
+
if (typeof process !== "undefined" && process.stderr && typeof process.stderr.write === "function") {
|
|
1266
|
+
process.stderr.write(`mdocs index skipped ${path}: ${detail}
|
|
1267
|
+
`);
|
|
1268
|
+
}
|
|
1946
1269
|
}
|
|
1947
1270
|
async function readManifest(io) {
|
|
1948
1271
|
const raw = await io.readText(MANIFEST_PATH);
|
|
@@ -1952,79 +1275,21 @@ async function writeManifest(io, manifest) {
|
|
|
1952
1275
|
await writeJsonText(io, MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
|
|
1953
1276
|
`);
|
|
1954
1277
|
}
|
|
1955
|
-
async function readSidecar(io, docId) {
|
|
1956
|
-
const raw = await io.readText(sidecarPath(docId));
|
|
1957
|
-
return JSON.parse(raw);
|
|
1958
|
-
}
|
|
1959
|
-
async function writeSidecar(io, sidecar) {
|
|
1960
|
-
await writeJsonText(io, sidecarPath(sidecar.docId), `${JSON.stringify(sidecar, null, 2)}
|
|
1961
|
-
`);
|
|
1962
|
-
}
|
|
1963
|
-
async function writeJsonText(io, path, content) {
|
|
1964
|
-
if (io.writeTextAtomic) {
|
|
1965
|
-
await io.writeTextAtomic(path, content);
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
await io.writeText(path, content);
|
|
1969
|
-
}
|
|
1970
1278
|
function findDoc(manifest, pathOrDocId) {
|
|
1971
1279
|
const entry = manifest.docs.find((doc) => doc.path === pathOrDocId || doc.docId === pathOrDocId);
|
|
1972
|
-
if (!entry) throw new
|
|
1280
|
+
if (!entry) throw new MdocsError("not_found", `Unknown document: ${pathOrDocId}`);
|
|
1973
1281
|
return entry;
|
|
1974
1282
|
}
|
|
1975
1283
|
async function readManifestIfPresent(io) {
|
|
1976
1284
|
if (!await io.exists(MANIFEST_PATH)) return void 0;
|
|
1977
1285
|
return readManifest(io);
|
|
1978
1286
|
}
|
|
1979
|
-
async function
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const sidecar = await readSidecar(io, entry.docId).catch(() => createEmptySidecar(entry));
|
|
1983
|
-
await writeSidecar(io, {
|
|
1984
|
-
...sidecar,
|
|
1985
|
-
path: entry.path,
|
|
1986
|
-
title: entry.title,
|
|
1987
|
-
anchors: remapAnchors(markdown, sidecar.anchors),
|
|
1988
|
-
updatedAt: nowIso()
|
|
1989
|
-
});
|
|
1287
|
+
async function writeJsonText(io, path, content) {
|
|
1288
|
+
if (io.writeTextAtomic) {
|
|
1289
|
+
await io.writeTextAtomic(path, content);
|
|
1990
1290
|
return;
|
|
1991
1291
|
}
|
|
1992
|
-
await
|
|
1993
|
-
}
|
|
1994
|
-
async function findRenamedDocumentEntry(io, candidates, markdown, contentHash, newPathCountForHash) {
|
|
1995
|
-
if (newPathCountForHash === 1) {
|
|
1996
|
-
const hashMatches = candidates.filter((entry) => entry.contentHash === contentHash);
|
|
1997
|
-
if (hashMatches.length === 1) return hashMatches[0];
|
|
1998
|
-
}
|
|
1999
|
-
const scored = [];
|
|
2000
|
-
for (const entry of candidates) {
|
|
2001
|
-
const path = sidecarPath(entry.docId);
|
|
2002
|
-
if (!await io.exists(path)) continue;
|
|
2003
|
-
const sidecar = await readSidecar(io, entry.docId).catch(() => void 0);
|
|
2004
|
-
if (!sidecar) continue;
|
|
2005
|
-
const score = sidecarMarkdownMatchScore(sidecar, markdown);
|
|
2006
|
-
if (score > 0) scored.push({ entry, score });
|
|
2007
|
-
}
|
|
2008
|
-
scored.sort((left, right) => right.score - left.score);
|
|
2009
|
-
const best = scored[0];
|
|
2010
|
-
const runnerUp = scored[1];
|
|
2011
|
-
return best && best.score > (runnerUp?.score ?? 0) ? best.entry : void 0;
|
|
2012
|
-
}
|
|
2013
|
-
function sidecarMarkdownMatchScore(sidecar, markdown) {
|
|
2014
|
-
const needles = /* @__PURE__ */ new Set();
|
|
2015
|
-
for (const anchor of sidecar.anchors) {
|
|
2016
|
-
const quote = anchor.selector.quote.trim();
|
|
2017
|
-
if (quote) needles.add(quote);
|
|
2018
|
-
}
|
|
2019
|
-
for (const suggestion of sidecar.suggestions) {
|
|
2020
|
-
if (suggestion.patch.before.trim()) needles.add(suggestion.patch.before.trim());
|
|
2021
|
-
if (suggestion.patch.after.trim()) needles.add(suggestion.patch.after.trim());
|
|
2022
|
-
}
|
|
2023
|
-
let score = 0;
|
|
2024
|
-
for (const needle of needles) {
|
|
2025
|
-
if (markdown.includes(needle)) score += 1;
|
|
2026
|
-
}
|
|
2027
|
-
return score;
|
|
1292
|
+
await io.writeText(path, content);
|
|
2028
1293
|
}
|
|
2029
1294
|
|
|
2030
1295
|
// ../core/src/context.ts
|
|
@@ -2043,8 +1308,6 @@ function sliceMarkdown(markdown, startLine, endLine) {
|
|
|
2043
1308
|
return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
|
|
2044
1309
|
}
|
|
2045
1310
|
function summarizeDocumentContext(document) {
|
|
2046
|
-
const comments = openComments(document);
|
|
2047
|
-
const suggestions = openSuggestions(document);
|
|
2048
1311
|
const currentSha = currentDocumentSha(document);
|
|
2049
1312
|
return {
|
|
2050
1313
|
docId: document.docId,
|
|
@@ -2055,9 +1318,9 @@ function summarizeDocumentContext(document) {
|
|
|
2055
1318
|
byteLength: new TextEncoder().encode(document.markdown).byteLength,
|
|
2056
1319
|
headings: extractMarkdownHeadings(document.markdown),
|
|
2057
1320
|
reviewCounts: {
|
|
2058
|
-
openComments:
|
|
2059
|
-
openSuggestions:
|
|
2060
|
-
anchorsNeedingReview: document.
|
|
1321
|
+
openComments: document.openComments,
|
|
1322
|
+
openSuggestions: document.openSuggestions,
|
|
1323
|
+
anchorsNeedingReview: document.anchorsNeedingReview
|
|
2061
1324
|
},
|
|
2062
1325
|
imageCounts: imageCounts(document.images),
|
|
2063
1326
|
linkCounts: linkCounts(document.links)
|
|
@@ -2070,9 +1333,12 @@ function reviewStateForDocument(document) {
|
|
|
2070
1333
|
path: document.path,
|
|
2071
1334
|
title: document.title,
|
|
2072
1335
|
...currentSha ? { currentSha } : {},
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
1336
|
+
reviewCounts: {
|
|
1337
|
+
openComments: document.openComments,
|
|
1338
|
+
openSuggestions: document.openSuggestions,
|
|
1339
|
+
anchorsNeedingReview: document.anchorsNeedingReview
|
|
1340
|
+
},
|
|
1341
|
+
message: "Review state is authority-backed. Use remote authority commands or MCP endpoints for comments, suggestions, accept, and reject."
|
|
2076
1342
|
};
|
|
2077
1343
|
}
|
|
2078
1344
|
function currentDocumentSha(document) {
|
|
@@ -2101,12 +1367,6 @@ function extractMarkdownHeadings(markdown) {
|
|
|
2101
1367
|
});
|
|
2102
1368
|
return headings;
|
|
2103
1369
|
}
|
|
2104
|
-
function openComments(document) {
|
|
2105
|
-
return document.sidecar.comments.filter((comment2) => comment2.status === "open");
|
|
2106
|
-
}
|
|
2107
|
-
function openSuggestions(document) {
|
|
2108
|
-
return document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open");
|
|
2109
|
-
}
|
|
2110
1370
|
function imageCounts(images) {
|
|
2111
1371
|
const local = images.filter((image2) => image2.isLocal).length;
|
|
2112
1372
|
return {
|
|
@@ -4844,7 +4104,8 @@ var canonicalSchema = new Schema({
|
|
|
4844
4104
|
nodes: {
|
|
4845
4105
|
// The doc allows suggestion marks on its block children so pending
|
|
4846
4106
|
// block-level suggestions (node marks) survive fromJSON and can be
|
|
4847
|
-
// structurally
|
|
4107
|
+
// structurally resolved before serialization. Only inline insert/delete is
|
|
4108
|
+
// used today; block-level (modification) is reserved for a later pass.
|
|
4848
4109
|
doc: { content: "block+", marks: "insertion deletion modification" },
|
|
4849
4110
|
paragraph: { group: "block", content: "inline*", attrs: blockAttrs },
|
|
4850
4111
|
heading: {
|
|
@@ -4926,11 +4187,14 @@ var canonicalSchema = new Schema({
|
|
|
4926
4187
|
strike: {},
|
|
4927
4188
|
code: {},
|
|
4928
4189
|
// Pending-suggestion marks (mirroring the web editor's extensions). They
|
|
4929
|
-
//
|
|
4930
|
-
// but the schema must know them so suggestion-marked docs load
|
|
4190
|
+
// never appear in canonical bytes — the canonical serializer resolves them
|
|
4191
|
+
// first — but the schema must know them so suggestion-marked docs load and
|
|
4192
|
+
// the CriticMarkup codec can emit/parse them. `id` tags a rendered span
|
|
4193
|
+
// with its owning branch at overlay time; it is empty while editing one
|
|
4194
|
+
// branch.
|
|
4931
4195
|
insertion: {
|
|
4932
4196
|
attrs: { id: { default: null } },
|
|
4933
|
-
inclusive:
|
|
4197
|
+
inclusive: true,
|
|
4934
4198
|
excludes: "deletion modification insertion"
|
|
4935
4199
|
},
|
|
4936
4200
|
deletion: {
|
|
@@ -11016,6 +10280,69 @@ var MarkdownSerializerState = class {
|
|
|
11016
10280
|
}
|
|
11017
10281
|
};
|
|
11018
10282
|
|
|
10283
|
+
// ../core/src/suggestion-id.ts
|
|
10284
|
+
var SUGGESTION_ID_PREFIX = "suggestion:";
|
|
10285
|
+
|
|
10286
|
+
// ../core/src/critic-markup.ts
|
|
10287
|
+
var INSERTION_OPEN_SENTINEL = String.fromCharCode(57344);
|
|
10288
|
+
var INSERTION_CLOSE_SENTINEL = String.fromCharCode(57345);
|
|
10289
|
+
var DELETION_OPEN_SENTINEL = String.fromCharCode(57346);
|
|
10290
|
+
var DELETION_CLOSE_SENTINEL = String.fromCharCode(57347);
|
|
10291
|
+
var ID_OPEN_SENTINEL = String.fromCharCode(57348);
|
|
10292
|
+
var ID_CLOSE_SENTINEL = String.fromCharCode(57349);
|
|
10293
|
+
function suggestionSpanAt(src, start) {
|
|
10294
|
+
const open = src.slice(start, start + 3);
|
|
10295
|
+
if (open === "{++") return { markName: "insertion", closeSequence: "++}" };
|
|
10296
|
+
if (open === "{--") return { markName: "deletion", closeSequence: "--}" };
|
|
10297
|
+
return void 0;
|
|
10298
|
+
}
|
|
10299
|
+
function suggestionIdSuffixAt(src, pos) {
|
|
10300
|
+
if (!src.startsWith("{#", pos)) return void 0;
|
|
10301
|
+
const end = src.indexOf("}", pos + 2);
|
|
10302
|
+
if (end === -1) return void 0;
|
|
10303
|
+
const id = src.slice(pos + 2, end);
|
|
10304
|
+
return id.startsWith(SUGGESTION_ID_PREFIX) ? id : void 0;
|
|
10305
|
+
}
|
|
10306
|
+
function findUnescapedClose(src, from, closeSequence) {
|
|
10307
|
+
let pos = from;
|
|
10308
|
+
while (pos < src.length) {
|
|
10309
|
+
if (src.charCodeAt(pos) === 92) {
|
|
10310
|
+
pos += 2;
|
|
10311
|
+
continue;
|
|
10312
|
+
}
|
|
10313
|
+
if (src.startsWith(closeSequence, pos)) return pos;
|
|
10314
|
+
pos += 1;
|
|
10315
|
+
}
|
|
10316
|
+
return -1;
|
|
10317
|
+
}
|
|
10318
|
+
function criticMarkupPlugin(md) {
|
|
10319
|
+
md.inline.ruler.before("emphasis", "criticmarkup", (state, silent) => {
|
|
10320
|
+
const start = state.pos;
|
|
10321
|
+
if (state.src.charCodeAt(start) !== 123) return false;
|
|
10322
|
+
const span = suggestionSpanAt(state.src, start);
|
|
10323
|
+
if (!span) return false;
|
|
10324
|
+
const contentStart = start + 3;
|
|
10325
|
+
const closeStart = findUnescapedClose(state.src, contentStart, span.closeSequence);
|
|
10326
|
+
if (closeStart === -1) return false;
|
|
10327
|
+
if (silent) return true;
|
|
10328
|
+
const openToken = state.push(`${span.markName}_open`, "", 1);
|
|
10329
|
+
const savedMax = state.posMax;
|
|
10330
|
+
state.pos = contentStart;
|
|
10331
|
+
state.posMax = closeStart;
|
|
10332
|
+
state.md.inline.tokenize(state);
|
|
10333
|
+
state.posMax = savedMax;
|
|
10334
|
+
state.push(`${span.markName}_close`, "", -1);
|
|
10335
|
+
let nextPos = closeStart + span.closeSequence.length;
|
|
10336
|
+
const id = suggestionIdSuffixAt(state.src, nextPos);
|
|
10337
|
+
if (id !== void 0) {
|
|
10338
|
+
openToken.attrSet("id", id);
|
|
10339
|
+
nextPos += `{#${id}}`.length;
|
|
10340
|
+
}
|
|
10341
|
+
state.pos = nextPos;
|
|
10342
|
+
return true;
|
|
10343
|
+
});
|
|
10344
|
+
}
|
|
10345
|
+
|
|
11019
10346
|
// ../core/src/pm-markdown.ts
|
|
11020
10347
|
function serializeInlineFragmentToMarkdown(node) {
|
|
11021
10348
|
const paragraph2 = canonicalSchema.nodes.paragraph;
|
|
@@ -11058,109 +10385,119 @@ function backtickFence(node) {
|
|
|
11058
10385
|
const longest = matches.reduce((length, run) => Math.max(length, run.length), 2);
|
|
11059
10386
|
return "`".repeat(longest + 1);
|
|
11060
10387
|
}
|
|
11061
|
-
var
|
|
11062
|
-
{
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
|
|
10388
|
+
var NODE_SERIALIZERS = {
|
|
10389
|
+
paragraph(state, node) {
|
|
10390
|
+
state.renderInline(node);
|
|
10391
|
+
state.closeBlock(node);
|
|
10392
|
+
},
|
|
10393
|
+
heading(state, node) {
|
|
10394
|
+
state.write(`${"#".repeat(Number(node.attrs.level) || 1)} `);
|
|
10395
|
+
state.renderInline(node, false);
|
|
10396
|
+
state.closeBlock(node);
|
|
10397
|
+
},
|
|
10398
|
+
blockquote(state, node) {
|
|
10399
|
+
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
|
10400
|
+
},
|
|
10401
|
+
codeBlock(state, node) {
|
|
10402
|
+
const fence2 = backtickFence(node);
|
|
10403
|
+
state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
|
|
11078
10404
|
`);
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
}
|
|
10405
|
+
state.text(node.textContent, false);
|
|
10406
|
+
state.ensureNewLine();
|
|
10407
|
+
state.write(fence2);
|
|
10408
|
+
state.closeBlock(node);
|
|
10409
|
+
},
|
|
10410
|
+
horizontalRule(state, node) {
|
|
10411
|
+
state.write("---");
|
|
10412
|
+
state.closeBlock(node);
|
|
10413
|
+
},
|
|
10414
|
+
bulletList(state, node) {
|
|
10415
|
+
state.renderList(node, " ", () => "- ");
|
|
10416
|
+
},
|
|
10417
|
+
orderedList(state, node) {
|
|
10418
|
+
const start = Number(node.attrs.start) || 1;
|
|
10419
|
+
const maxWidth = String(start + node.childCount - 1).length;
|
|
10420
|
+
const space = state.repeat(" ", maxWidth + 2);
|
|
10421
|
+
state.renderList(node, space, (index) => {
|
|
10422
|
+
const label = String(start + index);
|
|
10423
|
+
return `${state.repeat(" ", maxWidth - label.length)}${label}. `;
|
|
10424
|
+
});
|
|
10425
|
+
},
|
|
10426
|
+
listItem(state, node) {
|
|
10427
|
+
state.renderContent(node);
|
|
10428
|
+
},
|
|
10429
|
+
taskList(state, node) {
|
|
10430
|
+
state.renderList(node, " ", () => "- ");
|
|
10431
|
+
},
|
|
10432
|
+
taskItem(state, node) {
|
|
10433
|
+
state.write(`[${node.attrs.checked ? "x" : " "}] `);
|
|
10434
|
+
state.renderContent(node);
|
|
10435
|
+
},
|
|
10436
|
+
table: serializeTable,
|
|
10437
|
+
image(state, node) {
|
|
10438
|
+
const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
|
|
10439
|
+
const alt = typeof node.attrs.alt === "string" ? node.attrs.alt : "";
|
|
10440
|
+
const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
|
|
10441
|
+
const destination = src.replace(/[()"]/g, "\\$&");
|
|
10442
|
+
const titleSuffix = title ? ` "${title.replace(/["\\]/g, "\\$&")}"` : "";
|
|
10443
|
+
state.write(``);
|
|
10444
|
+
},
|
|
10445
|
+
hardBreak(state, node, parent, index) {
|
|
10446
|
+
for (let after = index + 1; after < parent.childCount; after += 1) {
|
|
10447
|
+
if (parent.child(after).type !== node.type) {
|
|
10448
|
+
state.write("\\\n");
|
|
10449
|
+
return;
|
|
11125
10450
|
}
|
|
11126
|
-
},
|
|
11127
|
-
text(state, node) {
|
|
11128
|
-
state.text(node.text ?? "");
|
|
11129
10451
|
}
|
|
11130
10452
|
},
|
|
11131
|
-
{
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
escape: false
|
|
10453
|
+
text(state, node) {
|
|
10454
|
+
state.text(node.text ?? "");
|
|
10455
|
+
}
|
|
10456
|
+
};
|
|
10457
|
+
var BASE_MARK_SERIALIZERS = {
|
|
10458
|
+
bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
|
|
10459
|
+
italic: { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true },
|
|
10460
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
10461
|
+
code: {
|
|
10462
|
+
open(_state, _mark, parent, index) {
|
|
10463
|
+
return backticksFor2(parent.child(index), -1);
|
|
11143
10464
|
},
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
close(_state, mark) {
|
|
11147
|
-
const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
|
|
11148
|
-
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
11149
|
-
},
|
|
11150
|
-
mixable: false
|
|
10465
|
+
close(_state, _mark, parent, index) {
|
|
10466
|
+
return backticksFor2(parent.child(index - 1), 1);
|
|
11151
10467
|
},
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
}
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
10468
|
+
escape: false
|
|
10469
|
+
},
|
|
10470
|
+
link: {
|
|
10471
|
+
open: "[",
|
|
10472
|
+
close(_state, mark) {
|
|
10473
|
+
const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
|
|
10474
|
+
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
10475
|
+
},
|
|
10476
|
+
mixable: false
|
|
10477
|
+
},
|
|
10478
|
+
wikilink: {
|
|
10479
|
+
open(_state, mark, parent, index) {
|
|
10480
|
+
const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
|
|
10481
|
+
const label = markedRangeText(parent, index, mark);
|
|
10482
|
+
if (!target || label === target) return "[[";
|
|
10483
|
+
return `[[${target}|`;
|
|
10484
|
+
},
|
|
10485
|
+
close: "]]",
|
|
10486
|
+
mixable: false
|
|
11162
10487
|
}
|
|
11163
|
-
|
|
10488
|
+
};
|
|
10489
|
+
var markdownSerializer = new MarkdownSerializer(NODE_SERIALIZERS, BASE_MARK_SERIALIZERS);
|
|
10490
|
+
function suggestionMarkClose(closeSentinel) {
|
|
10491
|
+
return (_state, mark) => {
|
|
10492
|
+
const id = typeof mark.attrs.id === "string" ? mark.attrs.id : "";
|
|
10493
|
+
return id ? `${closeSentinel}${ID_OPEN_SENTINEL}${id}${ID_CLOSE_SENTINEL}` : closeSentinel;
|
|
10494
|
+
};
|
|
10495
|
+
}
|
|
10496
|
+
var criticMarkupSerializer = new MarkdownSerializer(NODE_SERIALIZERS, {
|
|
10497
|
+
...BASE_MARK_SERIALIZERS,
|
|
10498
|
+
insertion: { open: INSERTION_OPEN_SENTINEL, close: suggestionMarkClose(INSERTION_CLOSE_SENTINEL), mixable: true },
|
|
10499
|
+
deletion: { open: DELETION_OPEN_SENTINEL, close: suggestionMarkClose(DELETION_CLOSE_SENTINEL), mixable: true }
|
|
10500
|
+
});
|
|
11164
10501
|
function markedRangeText(parent, index, mark) {
|
|
11165
10502
|
let text2 = "";
|
|
11166
10503
|
for (let childIndex = index; childIndex < parent.childCount; childIndex += 1) {
|
|
@@ -11187,7 +10524,7 @@ function backticksFor2(node, side) {
|
|
|
11187
10524
|
}
|
|
11188
10525
|
|
|
11189
10526
|
// ../core/src/pm-parse.ts
|
|
11190
|
-
var
|
|
10527
|
+
var BASE_TOKEN_SPEC = {
|
|
11191
10528
|
blockquote: { block: "blockquote" },
|
|
11192
10529
|
paragraph: { block: "paragraph" },
|
|
11193
10530
|
list_item: { block: "listItem" },
|
|
@@ -11214,56 +10551,246 @@ var parser = new MarkdownParser(canonicalSchema, new lib_default(), {
|
|
|
11214
10551
|
title: tok.attrGet("title")
|
|
11215
10552
|
})
|
|
11216
10553
|
}
|
|
10554
|
+
};
|
|
10555
|
+
var parser = new MarkdownParser(canonicalSchema, new lib_default(), BASE_TOKEN_SPEC);
|
|
10556
|
+
var criticMarkupParser = new MarkdownParser(canonicalSchema, new lib_default().use(criticMarkupPlugin), {
|
|
10557
|
+
...BASE_TOKEN_SPEC,
|
|
10558
|
+
insertion: { mark: "insertion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) },
|
|
10559
|
+
deletion: { mark: "deletion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) }
|
|
11217
10560
|
});
|
|
11218
10561
|
|
|
11219
|
-
// ../core/src/
|
|
11220
|
-
var
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
10562
|
+
// ../core/src/patch.ts
|
|
10563
|
+
var PATCH_SCHEMA = "magic-markdown.patch.v1";
|
|
10564
|
+
function textPatchHash(text2) {
|
|
10565
|
+
return contentHashForText(text2);
|
|
10566
|
+
}
|
|
10567
|
+
function buildPatchEnvelope(options) {
|
|
10568
|
+
const ops = diffToPatchOps(options.base, options.result);
|
|
10569
|
+
return patchEnvelopeFromOps({
|
|
10570
|
+
baseVersion: options.baseVersion,
|
|
10571
|
+
base: options.base,
|
|
10572
|
+
ops,
|
|
10573
|
+
clientMutationId: options.clientMutationId,
|
|
10574
|
+
clientMeta: options.clientMeta
|
|
10575
|
+
});
|
|
10576
|
+
}
|
|
10577
|
+
function buildLineRangePatchEnvelope(options) {
|
|
10578
|
+
assertLineRangeWithin(options.base, options.range);
|
|
10579
|
+
const result = replaceLineRange(options.base, options.range, options.replacement);
|
|
10580
|
+
return buildPatchEnvelope({
|
|
10581
|
+
baseVersion: options.baseVersion,
|
|
10582
|
+
base: options.base,
|
|
10583
|
+
result,
|
|
10584
|
+
clientMutationId: options.clientMutationId,
|
|
10585
|
+
clientMeta: { ...options.clientMeta ?? {}, source: options.clientMeta?.source ?? "line_range" }
|
|
10586
|
+
});
|
|
10587
|
+
}
|
|
10588
|
+
function patchEnvelopeFromOps(options) {
|
|
10589
|
+
const ops = normalizePatchOps(options.ops);
|
|
10590
|
+
const result = applyPatchOps(options.base, ops);
|
|
10591
|
+
return {
|
|
10592
|
+
schema: PATCH_SCHEMA,
|
|
10593
|
+
baseVersion: options.baseVersion,
|
|
10594
|
+
baseHash: textPatchHash(options.base),
|
|
10595
|
+
baseLength: options.base.length,
|
|
10596
|
+
resultLength: result.length,
|
|
10597
|
+
resultHash: textPatchHash(result),
|
|
10598
|
+
ops,
|
|
10599
|
+
...options.clientMutationId ? { clientMutationId: options.clientMutationId } : {},
|
|
10600
|
+
...options.clientMeta ? { clientMeta: options.clientMeta } : {}
|
|
10601
|
+
};
|
|
10602
|
+
}
|
|
10603
|
+
function applyPatchOps(base, ops) {
|
|
10604
|
+
validatePatchOpsShape(ops);
|
|
10605
|
+
let baseCursor = 0;
|
|
10606
|
+
let result = "";
|
|
10607
|
+
for (const op of ops) {
|
|
10608
|
+
if (op.type === "retain") {
|
|
10609
|
+
const next = baseCursor + op.count;
|
|
10610
|
+
if (next > base.length) throw new MdocsError("validation_error", "Patch retain runs past the base document.");
|
|
10611
|
+
result += base.slice(baseCursor, next);
|
|
10612
|
+
baseCursor = next;
|
|
10613
|
+
continue;
|
|
10614
|
+
}
|
|
10615
|
+
if (op.type === "delete") {
|
|
10616
|
+
const actual = base.slice(baseCursor, baseCursor + op.text.length);
|
|
10617
|
+
if (actual !== op.text) throw new MdocsError("conflict", "Patch delete text does not match the base document.");
|
|
10618
|
+
baseCursor += op.text.length;
|
|
10619
|
+
continue;
|
|
10620
|
+
}
|
|
10621
|
+
result += op.text;
|
|
10622
|
+
}
|
|
10623
|
+
return result + base.slice(baseCursor);
|
|
10624
|
+
}
|
|
10625
|
+
function normalizePatchOps(ops) {
|
|
10626
|
+
validatePatchOpsShape(ops);
|
|
10627
|
+
const normalized = [];
|
|
10628
|
+
for (const op of ops) {
|
|
10629
|
+
if (op.type === "retain" && op.count === 0) continue;
|
|
10630
|
+
if ((op.type === "insert" || op.type === "delete") && op.text.length === 0) continue;
|
|
10631
|
+
const last = normalized.at(-1);
|
|
10632
|
+
if (last?.type === op.type) {
|
|
10633
|
+
if (op.type === "retain" && last.type === "retain") last.count += op.count;
|
|
10634
|
+
if (op.type === "insert" && last.type === "insert") last.text += op.text;
|
|
10635
|
+
if (op.type === "delete" && last.type === "delete") last.text += op.text;
|
|
10636
|
+
continue;
|
|
10637
|
+
}
|
|
10638
|
+
normalized.push({ ...op });
|
|
11243
10639
|
}
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
10640
|
+
return normalized;
|
|
10641
|
+
}
|
|
10642
|
+
function diffToPatchOps(base, result) {
|
|
10643
|
+
if (base === result) return [];
|
|
10644
|
+
let prefix = 0;
|
|
10645
|
+
while (prefix < base.length && prefix < result.length && base[prefix] === result[prefix]) prefix += 1;
|
|
10646
|
+
let suffix = 0;
|
|
10647
|
+
while (suffix < base.length - prefix && suffix < result.length - prefix && base[base.length - 1 - suffix] === result[result.length - 1 - suffix]) {
|
|
10648
|
+
suffix += 1;
|
|
10649
|
+
}
|
|
10650
|
+
const tokenRefined = diffMiddleByTokens(base, result, prefix, suffix);
|
|
10651
|
+
if (tokenRefined) return tokenRefined;
|
|
10652
|
+
const refined = diffMiddleByLines(base, result, prefix, suffix);
|
|
10653
|
+
if (refined) return refined;
|
|
10654
|
+
return singleWindowPatchOps(base, result, prefix, suffix);
|
|
10655
|
+
}
|
|
10656
|
+
function singleWindowPatchOps(base, result, prefix, suffix) {
|
|
10657
|
+
const ops = [];
|
|
10658
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10659
|
+
const deleted = base.slice(prefix, base.length - suffix);
|
|
10660
|
+
const inserted = result.slice(prefix, result.length - suffix);
|
|
10661
|
+
if (deleted.length > 0) ops.push({ type: "delete", text: deleted });
|
|
10662
|
+
if (inserted.length > 0) ops.push({ type: "insert", text: inserted });
|
|
10663
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10664
|
+
return normalizePatchOps(ops);
|
|
10665
|
+
}
|
|
10666
|
+
function diffMiddleByLines(base, result, prefix, suffix) {
|
|
10667
|
+
const baseMiddle = base.slice(prefix, base.length - suffix);
|
|
10668
|
+
const resultMiddle = result.slice(prefix, result.length - suffix);
|
|
10669
|
+
if (!baseMiddle.includes("\n") && !resultMiddle.includes("\n")) return void 0;
|
|
10670
|
+
const baseLines = splitLinesPreservingNewline(baseMiddle);
|
|
10671
|
+
const resultLines = splitLinesPreservingNewline(resultMiddle);
|
|
10672
|
+
if (baseLines.length + resultLines.length > 800) return void 0;
|
|
10673
|
+
if (!hasSharedLine(baseLines, resultLines)) return void 0;
|
|
10674
|
+
const middleOps = lineDiffOps(baseLines, resultLines);
|
|
10675
|
+
if (middleOps.length <= 2) return void 0;
|
|
10676
|
+
const ops = [];
|
|
10677
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10678
|
+
ops.push(...middleOps);
|
|
10679
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10680
|
+
return normalizePatchOps(ops);
|
|
10681
|
+
}
|
|
10682
|
+
function diffMiddleByTokens(base, result, prefix, suffix) {
|
|
10683
|
+
const baseMiddle = base.slice(prefix, base.length - suffix);
|
|
10684
|
+
const resultMiddle = result.slice(prefix, result.length - suffix);
|
|
10685
|
+
if (!baseMiddle || !resultMiddle) return void 0;
|
|
10686
|
+
const baseTokens = tokenizePatchMiddle(baseMiddle);
|
|
10687
|
+
const resultTokens = tokenizePatchMiddle(resultMiddle);
|
|
10688
|
+
if (baseTokens.length + resultTokens.length > 1200) return void 0;
|
|
10689
|
+
if (!hasSharedToken(baseTokens, resultTokens)) return void 0;
|
|
10690
|
+
const middleOps = tokenDiffOps(baseTokens, resultTokens);
|
|
10691
|
+
if (!middleOps.some((op) => op.type === "retain")) return void 0;
|
|
10692
|
+
const ops = [];
|
|
10693
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10694
|
+
ops.push(...middleOps);
|
|
10695
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10696
|
+
const normalized = normalizePatchOps(ops);
|
|
10697
|
+
return normalized.length > 3 ? normalized : void 0;
|
|
10698
|
+
}
|
|
10699
|
+
function tokenizePatchMiddle(text2) {
|
|
10700
|
+
return text2.match(/[\p{L}\p{N}_]+|\s+|[^\s\p{L}\p{N}_]+/gu) ?? [];
|
|
10701
|
+
}
|
|
10702
|
+
function hasSharedToken(first, second) {
|
|
10703
|
+
const secondSet = new Set(second);
|
|
10704
|
+
return first.some((token) => secondSet.has(token));
|
|
10705
|
+
}
|
|
10706
|
+
function tokenDiffOps(baseTokens, resultTokens) {
|
|
10707
|
+
const lcs = tokenLcsTable(baseTokens, resultTokens);
|
|
10708
|
+
const ops = [];
|
|
10709
|
+
let baseIndex = 0;
|
|
10710
|
+
let resultIndex = 0;
|
|
10711
|
+
while (baseIndex < baseTokens.length || resultIndex < resultTokens.length) {
|
|
10712
|
+
if (baseIndex < baseTokens.length && resultIndex < resultTokens.length && baseTokens[baseIndex] === resultTokens[resultIndex]) {
|
|
10713
|
+
ops.push({ type: "retain", count: baseTokens[baseIndex].length });
|
|
10714
|
+
baseIndex += 1;
|
|
10715
|
+
resultIndex += 1;
|
|
10716
|
+
continue;
|
|
10717
|
+
}
|
|
10718
|
+
if (resultIndex >= resultTokens.length || baseIndex < baseTokens.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
|
|
10719
|
+
ops.push({ type: "delete", text: baseTokens[baseIndex] });
|
|
10720
|
+
baseIndex += 1;
|
|
10721
|
+
continue;
|
|
10722
|
+
}
|
|
10723
|
+
ops.push({ type: "insert", text: resultTokens[resultIndex] });
|
|
10724
|
+
resultIndex += 1;
|
|
11250
10725
|
}
|
|
11251
|
-
|
|
11252
|
-
|
|
10726
|
+
return normalizePatchOps(ops);
|
|
10727
|
+
}
|
|
10728
|
+
function tokenLcsTable(baseTokens, resultTokens) {
|
|
10729
|
+
const table2 = Array.from({ length: baseTokens.length + 1 }, () => Array.from({ length: resultTokens.length + 1 }, () => 0));
|
|
10730
|
+
for (let baseIndex = baseTokens.length - 1; baseIndex >= 0; baseIndex -= 1) {
|
|
10731
|
+
for (let resultIndex = resultTokens.length - 1; resultIndex >= 0; resultIndex -= 1) {
|
|
10732
|
+
table2[baseIndex][resultIndex] = baseTokens[baseIndex] === resultTokens[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
|
|
10733
|
+
}
|
|
11253
10734
|
}
|
|
11254
|
-
|
|
11255
|
-
|
|
10735
|
+
return table2;
|
|
10736
|
+
}
|
|
10737
|
+
function splitLinesPreservingNewline(text2) {
|
|
10738
|
+
return text2.match(/[^\n]*\n|[^\n]+/g) ?? [];
|
|
10739
|
+
}
|
|
10740
|
+
function hasSharedLine(first, second) {
|
|
10741
|
+
const secondSet = new Set(second);
|
|
10742
|
+
return first.some((line) => secondSet.has(line));
|
|
10743
|
+
}
|
|
10744
|
+
function lineDiffOps(baseLines, resultLines) {
|
|
10745
|
+
const lcs = lineLcsTable(baseLines, resultLines);
|
|
10746
|
+
const ops = [];
|
|
10747
|
+
let baseIndex = 0;
|
|
10748
|
+
let resultIndex = 0;
|
|
10749
|
+
while (baseIndex < baseLines.length || resultIndex < resultLines.length) {
|
|
10750
|
+
if (baseIndex < baseLines.length && resultIndex < resultLines.length && baseLines[baseIndex] === resultLines[resultIndex]) {
|
|
10751
|
+
ops.push({ type: "retain", count: baseLines[baseIndex].length });
|
|
10752
|
+
baseIndex += 1;
|
|
10753
|
+
resultIndex += 1;
|
|
10754
|
+
continue;
|
|
10755
|
+
}
|
|
10756
|
+
if (resultIndex >= resultLines.length || baseIndex < baseLines.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
|
|
10757
|
+
ops.push({ type: "delete", text: baseLines[baseIndex] });
|
|
10758
|
+
baseIndex += 1;
|
|
10759
|
+
continue;
|
|
10760
|
+
}
|
|
10761
|
+
ops.push({ type: "insert", text: resultLines[resultIndex] });
|
|
10762
|
+
resultIndex += 1;
|
|
11256
10763
|
}
|
|
11257
|
-
|
|
11258
|
-
|
|
10764
|
+
return normalizePatchOps(ops);
|
|
10765
|
+
}
|
|
10766
|
+
function lineLcsTable(baseLines, resultLines) {
|
|
10767
|
+
const table2 = Array.from({ length: baseLines.length + 1 }, () => Array.from({ length: resultLines.length + 1 }, () => 0));
|
|
10768
|
+
for (let baseIndex = baseLines.length - 1; baseIndex >= 0; baseIndex -= 1) {
|
|
10769
|
+
for (let resultIndex = resultLines.length - 1; resultIndex >= 0; resultIndex -= 1) {
|
|
10770
|
+
table2[baseIndex][resultIndex] = baseLines[baseIndex] === resultLines[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
|
|
10771
|
+
}
|
|
11259
10772
|
}
|
|
11260
|
-
|
|
11261
|
-
|
|
10773
|
+
return table2;
|
|
10774
|
+
}
|
|
10775
|
+
function validatePatchOpsShape(ops) {
|
|
10776
|
+
if (!Array.isArray(ops)) throw new MdocsError("validation_error", "Patch ops must be an array.");
|
|
10777
|
+
for (const op of ops) {
|
|
10778
|
+
if (op.type === "retain") {
|
|
10779
|
+
if (!Number.isInteger(op.count) || op.count < 0) {
|
|
10780
|
+
throw new MdocsError("validation_error", "Patch retain count must be a non-negative integer.");
|
|
10781
|
+
}
|
|
10782
|
+
continue;
|
|
10783
|
+
}
|
|
10784
|
+
if (op.type === "insert" || op.type === "delete") {
|
|
10785
|
+
if (typeof op.text !== "string") throw new MdocsError("validation_error", `Patch ${op.type} text must be a string.`);
|
|
10786
|
+
continue;
|
|
10787
|
+
}
|
|
10788
|
+
throw new MdocsError("validation_error", `Unknown patch op type: ${op.type}.`);
|
|
11262
10789
|
}
|
|
11263
|
-
}
|
|
10790
|
+
}
|
|
11264
10791
|
|
|
11265
10792
|
// src/agent.ts
|
|
11266
|
-
var CLI_VERSION = "0.3.
|
|
10793
|
+
var CLI_VERSION = "0.3.25";
|
|
11267
10794
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
11268
10795
|
var AGENT_COMMANDS = [
|
|
11269
10796
|
{
|
|
@@ -11351,12 +10878,12 @@ var AGENT_COMMANDS = [
|
|
|
11351
10878
|
"Use remote status --json immediately after joining when the handoff includes expected scope/root/doc values. If it reports wrong_binding, stop and ask for the correct share or connector.",
|
|
11352
10879
|
"Use remote context --summary --json first on file-scoped joins and before reading large documents. It returns metadata, heading line numbers, current head, review counts, and suggested next commands without dumping Markdown.",
|
|
11353
10880
|
"remote context accepts --start-line and --end-line (1-based, inclusive) to page through large documents. The response always includes totalLines, startLine, and endLine \u2014 if totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
11354
|
-
"Use remote context --no-review when reading document content only; use remote review to list open comments
|
|
10881
|
+
"Use remote context --no-review when reading document content only; use remote review to list authority-projected open comments and suggestions separately.",
|
|
11355
10882
|
"remote map works on project and workspace-scoped joins. Workspace-scoped map lists every shared Home root; target duplicate paths as <rootId>:<path-or-docId>.",
|
|
11356
10883
|
"remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
|
|
11357
10884
|
"remote create-file and remote move-file require a project-scoped join and edit access. They write through the root sync API, so version history and conflict handling stay intact.",
|
|
11358
10885
|
"remote library, create-folder, update-folder, move-root, and invite-folder use the organization suite on top of the joined project. Folder invites require admin access on the joined root and can only target folders containing that root.",
|
|
11359
|
-
"remote comment/suggest never clobber concurrent edits:
|
|
10886
|
+
"remote comment/suggest never clobber concurrent edits: comments, suggestions, accept, and reject go through DocumentAuthority.",
|
|
11360
10887
|
"remote reject <suggestionId> withdraws a suggestion you submitted (for example, a duplicate). You can always withdraw your own; rejecting another author's suggestion requires edit access, and resolving a human's review work needs the user's explicit ask.",
|
|
11361
10888
|
"remote events returns truncated: true when older events were dropped from the bounded log; refetch with remote context --summary, then read needed content pages and remote review instead of trusting the gap.",
|
|
11362
10889
|
"Agents have suggest-only access by default: comments and suggestions work, direct content rewrites are rejected with a forbidden_role error unless a human grants edit access.",
|
|
@@ -11365,7 +10892,7 @@ var AGENT_COMMANDS = [
|
|
|
11365
10892
|
},
|
|
11366
10893
|
{
|
|
11367
10894
|
name: "doctor",
|
|
11368
|
-
summary: "Validate the workspace map
|
|
10895
|
+
summary: "Validate the workspace map and agent interface readiness.",
|
|
11369
10896
|
usage: "mdocs doctor --json",
|
|
11370
10897
|
output: "json",
|
|
11371
10898
|
mutates: true,
|
|
@@ -11393,7 +10920,7 @@ var AGENT_COMMANDS = [
|
|
|
11393
10920
|
},
|
|
11394
10921
|
{
|
|
11395
10922
|
name: "context",
|
|
11396
|
-
summary: "Return agent-ready
|
|
10923
|
+
summary: "Return agent-ready clean Markdown, image references, and resolved outgoing links.",
|
|
11397
10924
|
usage: "mdocs context <path|docId> [--summary] [--no-review] [--start-line N] [--end-line N] --json",
|
|
11398
10925
|
output: "json",
|
|
11399
10926
|
mutates: true,
|
|
@@ -11407,12 +10934,12 @@ var AGENT_COMMANDS = [
|
|
|
11407
10934
|
"--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
|
|
11408
10935
|
"The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
11409
10936
|
"--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document.",
|
|
11410
|
-
"--no-review omits
|
|
10937
|
+
"--no-review omits authority-review redirect metadata from content reads. Use remote review for comments and suggestions."
|
|
11411
10938
|
]
|
|
11412
10939
|
},
|
|
11413
10940
|
{
|
|
11414
10941
|
name: "review",
|
|
11415
|
-
summary: "Return
|
|
10942
|
+
summary: "Return local authority-review redirect metadata for one document without returning Markdown.",
|
|
11416
10943
|
usage: "mdocs review <path|docId> --json",
|
|
11417
10944
|
output: "json",
|
|
11418
10945
|
mutates: true,
|
|
@@ -11420,82 +10947,12 @@ var AGENT_COMMANDS = [
|
|
|
11420
10947
|
},
|
|
11421
10948
|
{
|
|
11422
10949
|
name: "state",
|
|
11423
|
-
summary: "Return
|
|
10950
|
+
summary: "Return clean Markdown document state for one document.",
|
|
11424
10951
|
usage: "mdocs state <path|docId> --json",
|
|
11425
10952
|
output: "json",
|
|
11426
10953
|
mutates: true,
|
|
11427
10954
|
examples: ["mdocs state docs/example.md --json"]
|
|
11428
10955
|
},
|
|
11429
|
-
{
|
|
11430
|
-
name: "comment",
|
|
11431
|
-
summary: "Create a sidecar comment without modifying clean Markdown.",
|
|
11432
|
-
usage: "mdocs comment <path|docId> --range <start:end> --body <text> --json",
|
|
11433
|
-
output: "json",
|
|
11434
|
-
mutates: true,
|
|
11435
|
-
examples: [
|
|
11436
|
-
'mdocs comment docs/example.md --range 3:5 --body "Check this claim." --json',
|
|
11437
|
-
'mdocs comment docs/example.md --range 3:5 --body-file /tmp/comment.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
|
|
11438
|
-
],
|
|
11439
|
-
notes: ["--range is 1-based and inclusive, validated against the current document line count."]
|
|
11440
|
-
},
|
|
11441
|
-
{
|
|
11442
|
-
name: "suggest",
|
|
11443
|
-
summary: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown.",
|
|
11444
|
-
usage: "mdocs suggest <path|docId> --range <start:end> --with <markdown> --message <text> --json",
|
|
11445
|
-
output: "json",
|
|
11446
|
-
mutates: true,
|
|
11447
|
-
examples: [
|
|
11448
|
-
'mdocs suggest docs/example.md --range 4:4 --with "Better line." --message "Tighten wording." --json',
|
|
11449
|
-
'mdocs suggest docs/example.md --range 4:9 --with-file /tmp/replacement.md --message-file /tmp/message.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
|
|
11450
|
-
],
|
|
11451
|
-
notes: [
|
|
11452
|
-
"--range is 1-based and inclusive, validated against the current document line count.",
|
|
11453
|
-
"The replacement text replaces the whole range; re-read context first so the range matches current content.",
|
|
11454
|
-
"Prefer the smallest coherent range for the intended revision. If only one sentence inside a line changes, keep the rest of that line identical; use longer ranges only when the broader rewrite is genuinely needed."
|
|
11455
|
-
]
|
|
11456
|
-
},
|
|
11457
|
-
{
|
|
11458
|
-
name: "comments",
|
|
11459
|
-
summary: "List all comment threads for a document.",
|
|
11460
|
-
usage: "mdocs comments <path|docId> --json",
|
|
11461
|
-
output: "json",
|
|
11462
|
-
mutates: true,
|
|
11463
|
-
examples: ["mdocs comments docs/example.md --json"]
|
|
11464
|
-
},
|
|
11465
|
-
{
|
|
11466
|
-
name: "suggestions",
|
|
11467
|
-
summary: "List all suggestions for a document.",
|
|
11468
|
-
usage: "mdocs suggestions <path|docId> --json",
|
|
11469
|
-
output: "json",
|
|
11470
|
-
mutates: true,
|
|
11471
|
-
examples: ["mdocs suggestions docs/example.md --json"]
|
|
11472
|
-
},
|
|
11473
|
-
{
|
|
11474
|
-
name: "accept",
|
|
11475
|
-
summary: "Apply and accept an existing suggestion.",
|
|
11476
|
-
usage: "mdocs accept <suggestionId> --json",
|
|
11477
|
-
output: "json",
|
|
11478
|
-
mutates: true,
|
|
11479
|
-
examples: ["mdocs accept suggestion_abc123 --json"],
|
|
11480
|
-
notes: ["Only use when the user explicitly asks to apply a suggestion."]
|
|
11481
|
-
},
|
|
11482
|
-
{
|
|
11483
|
-
name: "reject",
|
|
11484
|
-
summary: "Reject an existing suggestion.",
|
|
11485
|
-
usage: "mdocs reject <suggestionId> --json",
|
|
11486
|
-
output: "json",
|
|
11487
|
-
mutates: true,
|
|
11488
|
-
examples: ["mdocs reject suggestion_abc123 --json"],
|
|
11489
|
-
notes: ["Only use when the user explicitly asks to reject a suggestion."]
|
|
11490
|
-
},
|
|
11491
|
-
{
|
|
11492
|
-
name: "resolve-comment",
|
|
11493
|
-
summary: "Resolve an existing comment thread.",
|
|
11494
|
-
usage: "mdocs resolve-comment <commentId> --json",
|
|
11495
|
-
output: "json",
|
|
11496
|
-
mutates: true,
|
|
11497
|
-
examples: ["mdocs resolve-comment comment_abc123 --json"]
|
|
11498
|
-
},
|
|
11499
10956
|
{
|
|
11500
10957
|
name: "status",
|
|
11501
10958
|
summary: "Return local Git status plus the Magic Markdown workspace map.",
|
|
@@ -11626,14 +11083,14 @@ function getAgentGuidePayload() {
|
|
|
11626
11083
|
function getAgentSkillMarkdown() {
|
|
11627
11084
|
return `---
|
|
11628
11085
|
name: magic-markdown
|
|
11629
|
-
description: Use the Magic Markdown CLI or MCP server to read
|
|
11086
|
+
description: Use the Magic Markdown CLI or MCP server to read clean Markdown workspaces, and use remote authority commands for comments, suggestions, accept, and reject. Use when an agent needs to interact with Magic Markdown documents, library folders, image context, checkpoints, remote review state, or local source sync.
|
|
11630
11087
|
metadata:
|
|
11631
11088
|
short-description: Operate Magic Markdown via CLI/MCP
|
|
11632
11089
|
---
|
|
11633
11090
|
|
|
11634
11091
|
# Magic Markdown Agent Workflow
|
|
11635
11092
|
|
|
11636
|
-
Magic Markdown keeps Markdown files clean
|
|
11093
|
+
Magic Markdown keeps Markdown files clean. Review state lives in DocumentAuthority: comments, suggestions, accept, and reject must use \`mdocs remote ...\` or the remote MCP connector, not local \`.mdocs\` files.
|
|
11637
11094
|
|
|
11638
11095
|
## Install / Bootstrap
|
|
11639
11096
|
|
|
@@ -11653,7 +11110,7 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
11653
11110
|
4. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --summary --json\` instead. Project-scoped joins cover one root; workspace-scoped joins cover Home and can target duplicate paths as \`<rootId>:<path-or-docId>\`.
|
|
11654
11111
|
5. Run \`mdocs graph --json\` or project-scoped \`mdocs remote graph --json\` before broad edits to inspect the Obsidian-style document graph built from Markdown links and wikilinks.
|
|
11655
11112
|
6. For a document, run \`mdocs context <path|docId> --summary --json\` locally or \`mdocs remote context <path|docId> --summary --json\` for a joined share before reading full content. Then page Markdown with \`--start-line\` / \`--end-line\` and \`--no-review\` when you only need document text.
|
|
11656
|
-
7. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not hand-edit canonical Markdown files or
|
|
11113
|
+
7. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not hand-edit canonical Markdown files or local review metadata directly.
|
|
11657
11114
|
|
|
11658
11115
|
## Filesystem Bridge / Resume
|
|
11659
11116
|
|
|
@@ -11693,7 +11150,7 @@ If Magic and the local root both changed the same document while you were offlin
|
|
|
11693
11150
|
|
|
11694
11151
|
## Editing Rules
|
|
11695
11152
|
|
|
11696
|
-
- Comments
|
|
11153
|
+
- Comments and suggestions are review operations. They should not modify canonical Markdown until a suggestion is accepted through authority.
|
|
11697
11154
|
- \`--range <start:end>\` is 1-based and inclusive, and validated against the current document: read the document first and quote line numbers from what you just read.
|
|
11698
11155
|
- Prefer the smallest coherent suggestion range that contains the actual change. If one sentence, list item, table row, or short paragraph changes, suggest that unit rather than replacing surrounding unchanged paragraphs or sections.
|
|
11699
11156
|
- Broader paragraph, section, or multi-section rewrites are appropriate when the edit genuinely changes structure, ordering, transitions, or multiple interdependent ideas. Do not avoid a long edit when it is the clearest correct revision.
|
|
@@ -11878,14 +11335,14 @@ async function runDoctor(io, workspaceRoot) {
|
|
|
11878
11335
|
details: `${map.docs.length} Markdown document${map.docs.length === 1 ? "" : "s"} indexed.`
|
|
11879
11336
|
},
|
|
11880
11337
|
{
|
|
11881
|
-
name: "
|
|
11338
|
+
name: "authority_projection_mapping",
|
|
11882
11339
|
ok: warnings.length === 0,
|
|
11883
|
-
details: warnings.length === 0 ? "
|
|
11340
|
+
details: warnings.length === 0 ? "No authority projection mapping warnings." : `${warnings.length} document(s) have review targets needing projection attention.`
|
|
11884
11341
|
},
|
|
11885
11342
|
{
|
|
11886
11343
|
name: "agent_cli_contract",
|
|
11887
11344
|
ok: AGENT_COMMANDS.length > 0,
|
|
11888
|
-
details: "Agent guide, JSON commands, checkpoints, document context,
|
|
11345
|
+
details: "Agent guide, JSON commands, checkpoints, clean document context, and MCP stdio are available. Review mutations require remote authority commands."
|
|
11889
11346
|
}
|
|
11890
11347
|
];
|
|
11891
11348
|
return {
|
|
@@ -11947,6 +11404,14 @@ var NodeWorkspaceIO = class {
|
|
|
11947
11404
|
return false;
|
|
11948
11405
|
}
|
|
11949
11406
|
}
|
|
11407
|
+
async statFile(path) {
|
|
11408
|
+
try {
|
|
11409
|
+
const stats = await stat2(await this.resolveExistingPath(path));
|
|
11410
|
+
return { mtimeMs: stats.mtimeMs, size: stats.size };
|
|
11411
|
+
} catch {
|
|
11412
|
+
return void 0;
|
|
11413
|
+
}
|
|
11414
|
+
}
|
|
11950
11415
|
async mkdir(path) {
|
|
11951
11416
|
const target = await this.resolvePath(path);
|
|
11952
11417
|
await mkdir2(target, { recursive: true });
|
|
@@ -12001,6 +11466,9 @@ var PathMappedWorkspaceIO = class {
|
|
|
12001
11466
|
async exists(path) {
|
|
12002
11467
|
return this.local.exists(this.toReplicaPath(path));
|
|
12003
11468
|
}
|
|
11469
|
+
async statFile(path) {
|
|
11470
|
+
return this.local.statFile(this.toReplicaPath(path));
|
|
11471
|
+
}
|
|
12004
11472
|
async mkdir(path) {
|
|
12005
11473
|
await this.local.mkdir(this.toReplicaPath(path));
|
|
12006
11474
|
}
|
|
@@ -12098,28 +11566,6 @@ function optionalString(args, name) {
|
|
|
12098
11566
|
const value = args[name];
|
|
12099
11567
|
return typeof value === "string" ? value : void 0;
|
|
12100
11568
|
}
|
|
12101
|
-
function requiredNumber(args, name) {
|
|
12102
|
-
const value = args[name];
|
|
12103
|
-
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
12104
|
-
if (!Number.isFinite(number)) throw new Error(`Missing numeric tool argument: ${name}`);
|
|
12105
|
-
return number;
|
|
12106
|
-
}
|
|
12107
|
-
function actorFromArgs(args, fallback) {
|
|
12108
|
-
const id = optionalString(args, "actorId") ?? fallback.id;
|
|
12109
|
-
const name = optionalString(args, "actorName") ?? fallback.name;
|
|
12110
|
-
const email = optionalString(args, "actorEmail") ?? fallback.email;
|
|
12111
|
-
const kind = actorKindFromArg(args.actorKind) ?? fallback.kind;
|
|
12112
|
-
return {
|
|
12113
|
-
id,
|
|
12114
|
-
kind,
|
|
12115
|
-
name,
|
|
12116
|
-
...email ? { email } : {}
|
|
12117
|
-
};
|
|
12118
|
-
}
|
|
12119
|
-
function actorKindFromArg(value) {
|
|
12120
|
-
if (value === "human" || value === "agent" || value === "system") return value;
|
|
12121
|
-
return void 0;
|
|
12122
|
-
}
|
|
12123
11569
|
|
|
12124
11570
|
// src/mcp-resources.ts
|
|
12125
11571
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
@@ -12180,19 +11626,12 @@ async function contextForDocument(io, path, startLine, endLine, options = {}) {
|
|
|
12180
11626
|
markdown,
|
|
12181
11627
|
images: withMcpImageResources(io, state).images,
|
|
12182
11628
|
links: state.links,
|
|
12183
|
-
...options.includeReview === false ? {} :
|
|
11629
|
+
...options.includeReview === false ? {} : { review: reviewStateForDocument(state) }
|
|
12184
11630
|
};
|
|
12185
11631
|
}
|
|
12186
11632
|
async function reviewForDocument(io, path) {
|
|
12187
11633
|
return reviewStateForDocument(await getDocumentState(io, path));
|
|
12188
11634
|
}
|
|
12189
|
-
function reviewFields(state) {
|
|
12190
|
-
return {
|
|
12191
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
12192
|
-
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
12193
|
-
anchors: state.anchors
|
|
12194
|
-
};
|
|
12195
|
-
}
|
|
12196
11635
|
function imageResourceUri(docId, index) {
|
|
12197
11636
|
return `mdocs://documents/${docId}/images/${index}`;
|
|
12198
11637
|
}
|
|
@@ -12233,25 +11672,6 @@ var documentPathSchema = {
|
|
|
12233
11672
|
required: ["path"],
|
|
12234
11673
|
additionalProperties: false
|
|
12235
11674
|
};
|
|
12236
|
-
var actorProperties = {
|
|
12237
|
-
actorId: {
|
|
12238
|
-
type: "string",
|
|
12239
|
-
description: "Optional actor id to record in sidecar metadata."
|
|
12240
|
-
},
|
|
12241
|
-
actorName: {
|
|
12242
|
-
type: "string",
|
|
12243
|
-
description: "Optional actor display name to record in sidecar metadata."
|
|
12244
|
-
},
|
|
12245
|
-
actorKind: {
|
|
12246
|
-
type: "string",
|
|
12247
|
-
enum: ["human", "agent", "system"],
|
|
12248
|
-
description: "Optional actor kind to record in sidecar metadata."
|
|
12249
|
-
},
|
|
12250
|
-
actorEmail: {
|
|
12251
|
-
type: "string",
|
|
12252
|
-
description: "Optional actor email to record in sidecar metadata."
|
|
12253
|
-
}
|
|
12254
|
-
};
|
|
12255
11675
|
var tools = [
|
|
12256
11676
|
{
|
|
12257
11677
|
name: "mdocs_map",
|
|
@@ -12270,101 +11690,41 @@ var tools = [
|
|
|
12270
11690
|
},
|
|
12271
11691
|
{
|
|
12272
11692
|
name: "mdocs_context",
|
|
12273
|
-
description: "Return agent-ready
|
|
12274
|
-
inputSchema: {
|
|
12275
|
-
type: "object",
|
|
12276
|
-
properties: {
|
|
12277
|
-
path: documentPathSchema.properties.path,
|
|
12278
|
-
startLine: {
|
|
12279
|
-
type: "number",
|
|
12280
|
-
description: "1-based first line to include (default: 1). The response startLine/endLine/totalLines fields tell you what was returned and whether more pages exist."
|
|
12281
|
-
},
|
|
12282
|
-
endLine: {
|
|
12283
|
-
type: "number",
|
|
12284
|
-
description: "1-based last line to include (default: last line of document)."
|
|
12285
|
-
},
|
|
12286
|
-
summary: {
|
|
12287
|
-
type: "boolean",
|
|
12288
|
-
description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
|
|
12289
|
-
},
|
|
12290
|
-
includeReview: {
|
|
12291
|
-
type: "boolean",
|
|
12292
|
-
description: "Whether to include open comments, suggestions, and anchors in the content response (default: true). Use false to keep document reading separate from review state."
|
|
12293
|
-
}
|
|
12294
|
-
},
|
|
12295
|
-
required: ["path"],
|
|
12296
|
-
additionalProperties: false
|
|
12297
|
-
}
|
|
12298
|
-
},
|
|
12299
|
-
{
|
|
12300
|
-
name: "mdocs_review",
|
|
12301
|
-
description: "Return open comments, open suggestions, and mapped anchors for one document without returning document Markdown.",
|
|
12302
|
-
inputSchema: documentPathSchema
|
|
12303
|
-
},
|
|
12304
|
-
{
|
|
12305
|
-
name: "mdocs_state",
|
|
12306
|
-
description: "Return full merged Markdown and sidecar state for one document.",
|
|
12307
|
-
inputSchema: documentPathSchema
|
|
12308
|
-
},
|
|
12309
|
-
{
|
|
12310
|
-
name: "mdocs_comment",
|
|
12311
|
-
description: "Create a sidecar comment without modifying clean Markdown.",
|
|
12312
|
-
inputSchema: {
|
|
12313
|
-
type: "object",
|
|
12314
|
-
properties: {
|
|
12315
|
-
path: documentPathSchema.properties.path,
|
|
12316
|
-
startLine: { type: "number", description: "1-based start line." },
|
|
12317
|
-
endLine: { type: "number", description: "1-based end line." },
|
|
12318
|
-
body: { type: "string", description: "Comment body." },
|
|
12319
|
-
...actorProperties
|
|
12320
|
-
},
|
|
12321
|
-
required: ["path", "startLine", "endLine", "body"],
|
|
12322
|
-
additionalProperties: false
|
|
12323
|
-
}
|
|
12324
|
-
},
|
|
12325
|
-
{
|
|
12326
|
-
name: "mdocs_suggest",
|
|
12327
|
-
description: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown. Prefer the smallest coherent range; use longer ranges only when the broader rewrite is genuinely needed.",
|
|
11693
|
+
description: "Return agent-ready clean Markdown and resolved outgoing links. Use summary=true first to inspect title, line count, headings, current head, and review/link/image counts without dumping the full document. Review mutations require the remote MCP connector or mdocs remote commands.",
|
|
12328
11694
|
inputSchema: {
|
|
12329
11695
|
type: "object",
|
|
12330
11696
|
properties: {
|
|
12331
11697
|
path: documentPathSchema.properties.path,
|
|
12332
|
-
startLine: {
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
11698
|
+
startLine: {
|
|
11699
|
+
type: "number",
|
|
11700
|
+
description: "1-based first line to include (default: 1). The response startLine/endLine/totalLines fields tell you what was returned and whether more pages exist."
|
|
11701
|
+
},
|
|
11702
|
+
endLine: {
|
|
11703
|
+
type: "number",
|
|
11704
|
+
description: "1-based last line to include (default: last line of document)."
|
|
11705
|
+
},
|
|
11706
|
+
summary: {
|
|
11707
|
+
type: "boolean",
|
|
11708
|
+
description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
|
|
11709
|
+
},
|
|
11710
|
+
includeReview: {
|
|
11711
|
+
type: "boolean",
|
|
11712
|
+
description: "Whether to include authority-review redirect metadata in the content response (default: true). Use false to keep document reads minimal."
|
|
11713
|
+
}
|
|
12337
11714
|
},
|
|
12338
|
-
required: ["path"
|
|
11715
|
+
required: ["path"],
|
|
12339
11716
|
additionalProperties: false
|
|
12340
11717
|
}
|
|
12341
11718
|
},
|
|
12342
11719
|
{
|
|
12343
|
-
name: "
|
|
12344
|
-
description: "
|
|
12345
|
-
inputSchema:
|
|
12346
|
-
type: "object",
|
|
12347
|
-
properties: {
|
|
12348
|
-
commentId: { type: "string", description: "Comment thread id." },
|
|
12349
|
-
...actorProperties
|
|
12350
|
-
},
|
|
12351
|
-
required: ["commentId"],
|
|
12352
|
-
additionalProperties: false
|
|
12353
|
-
}
|
|
11720
|
+
name: "mdocs_review",
|
|
11721
|
+
description: "Return local review redirect metadata for one document. Use remote MCP or mdocs remote review for authority-backed comments and suggestions.",
|
|
11722
|
+
inputSchema: documentPathSchema
|
|
12354
11723
|
},
|
|
12355
11724
|
{
|
|
12356
|
-
name: "
|
|
12357
|
-
description: "
|
|
12358
|
-
inputSchema:
|
|
12359
|
-
type: "object",
|
|
12360
|
-
properties: {
|
|
12361
|
-
suggestionId: { type: "string", description: "Suggestion id." },
|
|
12362
|
-
status: { type: "string", enum: ["accepted", "rejected"], description: "Resolution status." },
|
|
12363
|
-
...actorProperties
|
|
12364
|
-
},
|
|
12365
|
-
required: ["suggestionId", "status"],
|
|
12366
|
-
additionalProperties: false
|
|
12367
|
-
}
|
|
11725
|
+
name: "mdocs_state",
|
|
11726
|
+
description: "Return clean Markdown document state and metadata for one document.",
|
|
11727
|
+
inputSchema: documentPathSchema
|
|
12368
11728
|
},
|
|
12369
11729
|
{
|
|
12370
11730
|
name: "mdocs_diff",
|
|
@@ -12560,38 +11920,8 @@ async function callTool(io, name, args) {
|
|
|
12560
11920
|
}
|
|
12561
11921
|
if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
|
|
12562
11922
|
if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
|
|
12563
|
-
if (name === "mdocs_comment") {
|
|
12564
|
-
|
|
12565
|
-
io,
|
|
12566
|
-
requiredString(args, "path"),
|
|
12567
|
-
{ startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
|
|
12568
|
-
requiredString(args, "body"),
|
|
12569
|
-
actorFromArgs(args, defaultActor())
|
|
12570
|
-
);
|
|
12571
|
-
}
|
|
12572
|
-
if (name === "mdocs_suggest") {
|
|
12573
|
-
return addSuggestion(
|
|
12574
|
-
io,
|
|
12575
|
-
requiredString(args, "path"),
|
|
12576
|
-
{ startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
|
|
12577
|
-
requiredString(args, "replacement"),
|
|
12578
|
-
optionalString(args, "message") ?? "Suggested edit",
|
|
12579
|
-
actorFromArgs(args, {
|
|
12580
|
-
id: "agent_local",
|
|
12581
|
-
kind: "agent",
|
|
12582
|
-
name: "Local Agent"
|
|
12583
|
-
})
|
|
12584
|
-
);
|
|
12585
|
-
}
|
|
12586
|
-
if (name === "mdocs_resolve_suggestion") {
|
|
12587
|
-
const suggestionId = requiredString(args, "suggestionId");
|
|
12588
|
-
const status = requiredString(args, "status");
|
|
12589
|
-
if (status === "accepted") return acceptSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
|
|
12590
|
-
if (status === "rejected") return rejectSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
|
|
12591
|
-
throw new Error("status must be accepted or rejected");
|
|
12592
|
-
}
|
|
12593
|
-
if (name === "mdocs_resolve_comment") {
|
|
12594
|
-
return resolveComment(io, requiredString(args, "commentId"), actorFromArgs(args, defaultActor()));
|
|
11923
|
+
if (name === "mdocs_comment" || name === "mdocs_suggest" || name === "mdocs_resolve_suggestion" || name === "mdocs_resolve_comment") {
|
|
11924
|
+
throw new Error("Local MCP review mutations were removed. Use the remote MCP connector or `mdocs remote ...` so review state goes through DocumentAuthority.");
|
|
12595
11925
|
}
|
|
12596
11926
|
if (name === "mdocs_diff") {
|
|
12597
11927
|
return { patch: await gitDiff(io.root, optionalString(args, "path")) };
|
|
@@ -12802,33 +12132,47 @@ function splitWorkspaceDocumentTarget(value, rootIds) {
|
|
|
12802
12132
|
const target = value.slice(separator + 1);
|
|
12803
12133
|
return target ? { rootId, target } : void 0;
|
|
12804
12134
|
}
|
|
12805
|
-
async function
|
|
12806
|
-
|
|
12807
|
-
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/
|
|
12135
|
+
async function postSuggestionBranch(record, docId, input, actor) {
|
|
12136
|
+
return fetchJson(
|
|
12137
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions`,
|
|
12138
|
+
{
|
|
12139
|
+
method: "POST",
|
|
12140
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12141
|
+
body: JSON.stringify({ actor, ...input })
|
|
12142
|
+
}
|
|
12143
|
+
);
|
|
12144
|
+
}
|
|
12145
|
+
async function fetchAuthorityState(record, docId) {
|
|
12146
|
+
return fetchJson(
|
|
12147
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/authority-state`,
|
|
12148
|
+
{ headers: shareHeaders(record.shareUrl) }
|
|
12149
|
+
);
|
|
12150
|
+
}
|
|
12151
|
+
async function postResolveSuggestionBranch(record, docId, suggestionId, resolution, actor, changeId = randomUUID2()) {
|
|
12152
|
+
return fetchJson(
|
|
12153
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions/${encodeURIComponent(suggestionId)}/${resolution}`,
|
|
12808
12154
|
{
|
|
12809
12155
|
method: "POST",
|
|
12810
12156
|
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12811
|
-
body: JSON.stringify({ actor, changeId
|
|
12157
|
+
body: JSON.stringify({ actor, changeId })
|
|
12812
12158
|
}
|
|
12813
12159
|
);
|
|
12814
|
-
return response.document;
|
|
12815
12160
|
}
|
|
12816
|
-
async function
|
|
12161
|
+
async function postAuthorityComment(record, docId, input, actor) {
|
|
12817
12162
|
return fetchJson(
|
|
12818
|
-
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/
|
|
12163
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/comments`,
|
|
12819
12164
|
{
|
|
12820
12165
|
method: "POST",
|
|
12821
12166
|
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12822
|
-
body: JSON.stringify({ actor,
|
|
12167
|
+
body: JSON.stringify({ actor, ...input })
|
|
12823
12168
|
}
|
|
12824
12169
|
);
|
|
12825
12170
|
}
|
|
12826
|
-
async function pushDocument(record, baseDocument, markdown
|
|
12171
|
+
async function pushDocument(record, baseDocument, markdown) {
|
|
12827
12172
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12828
12173
|
const payload = {
|
|
12829
12174
|
...baseDocument,
|
|
12830
12175
|
markdown,
|
|
12831
|
-
sidecar,
|
|
12832
12176
|
baseHead: baseDocument.currentSha,
|
|
12833
12177
|
currentSha: baseDocument.currentSha
|
|
12834
12178
|
};
|
|
@@ -13081,6 +12425,7 @@ function joinStoreDir(root) {
|
|
|
13081
12425
|
}
|
|
13082
12426
|
|
|
13083
12427
|
// src/remote.ts
|
|
12428
|
+
var WORKSPACE_GRAPH_SCHEMA_VERSION = 1;
|
|
13084
12429
|
async function runJoinCommand(root, parsed) {
|
|
13085
12430
|
const target = parsed.command[1];
|
|
13086
12431
|
const existingId = typeof parsed.flags.id === "string" ? parsed.flags.id : void 0;
|
|
@@ -13131,7 +12476,7 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
13131
12476
|
if (record.scope === "workspace") return remoteWorkspaceMap(record);
|
|
13132
12477
|
const rootRecord = assertRootScoped(record, "map");
|
|
13133
12478
|
await refreshPresence(rootRecord, record.docId);
|
|
13134
|
-
return fetchTree(rootRecord);
|
|
12479
|
+
return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
13135
12480
|
}
|
|
13136
12481
|
case "graph":
|
|
13137
12482
|
return remoteGraph(record);
|
|
@@ -13225,28 +12570,35 @@ async function remoteGraph(record) {
|
|
|
13225
12570
|
assertProjectScope(record, "graph");
|
|
13226
12571
|
const rootRecord = assertRootScoped(record, "graph");
|
|
13227
12572
|
await refreshPresence(rootRecord, record.docId);
|
|
13228
|
-
const tree = await fetchTree(rootRecord);
|
|
12573
|
+
const tree = await treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
13229
12574
|
const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
|
|
12575
|
+
const authorityCounts = new Map(tree.docs.map((doc) => [doc.docId, doc]));
|
|
13230
12576
|
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
13231
12577
|
const graphDocuments = documents.map((doc) => {
|
|
13232
12578
|
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
12579
|
+
const counts = authorityCounts.get(doc.docId);
|
|
13233
12580
|
return {
|
|
13234
12581
|
docId: doc.docId,
|
|
13235
12582
|
path: doc.path,
|
|
13236
12583
|
title: doc.title,
|
|
13237
|
-
openComments:
|
|
13238
|
-
openSuggestions:
|
|
13239
|
-
anchorsNeedingReview:
|
|
12584
|
+
openComments: counts?.openComments ?? 0,
|
|
12585
|
+
openSuggestions: counts?.openSuggestions ?? 0,
|
|
12586
|
+
anchorsNeedingReview: counts?.anchorsNeedingReview ?? 0,
|
|
13240
12587
|
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
13241
12588
|
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
13242
12589
|
links
|
|
13243
12590
|
};
|
|
13244
12591
|
});
|
|
13245
|
-
return buildWorkspaceGraph(record.workspaceId,
|
|
12592
|
+
return buildWorkspaceGraph(record.workspaceId, WORKSPACE_GRAPH_SCHEMA_VERSION, graphDocuments);
|
|
13246
12593
|
}
|
|
13247
12594
|
async function remoteWorkspaceMap(record) {
|
|
13248
12595
|
const roots = await fetchWorkspaceRoots(record);
|
|
13249
|
-
const trees = await Promise.all(
|
|
12596
|
+
const trees = await Promise.all(
|
|
12597
|
+
roots.map(async (root) => {
|
|
12598
|
+
const rootRecord = rootScopedRecordFor(record, root.rootId);
|
|
12599
|
+
return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
12600
|
+
})
|
|
12601
|
+
);
|
|
13250
12602
|
const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
|
|
13251
12603
|
return {
|
|
13252
12604
|
workspaceId: record.workspaceId,
|
|
@@ -13270,6 +12622,27 @@ async function remoteWorkspaceMap(record) {
|
|
|
13270
12622
|
...library ? { folders: library.folders } : {}
|
|
13271
12623
|
};
|
|
13272
12624
|
}
|
|
12625
|
+
async function treeWithAuthorityCounts(record, tree) {
|
|
12626
|
+
const counts = await Promise.all(
|
|
12627
|
+
tree.docs.map(async (doc) => ({
|
|
12628
|
+
docId: doc.docId,
|
|
12629
|
+
counts: authorityReviewCounts(await fetchAuthorityState(record, doc.docId).catch(() => void 0))
|
|
12630
|
+
}))
|
|
12631
|
+
);
|
|
12632
|
+
const countsByDocId = new Map(counts.map((entry) => [entry.docId, entry.counts]));
|
|
12633
|
+
return {
|
|
12634
|
+
...tree,
|
|
12635
|
+
docs: tree.docs.map((doc) => {
|
|
12636
|
+
const review = countsByDocId.get(doc.docId);
|
|
12637
|
+
return review ? {
|
|
12638
|
+
...doc,
|
|
12639
|
+
openComments: review.openComments,
|
|
12640
|
+
openSuggestions: review.openSuggestions,
|
|
12641
|
+
anchorsNeedingReview: review.anchorsNeedingReview
|
|
12642
|
+
} : doc;
|
|
12643
|
+
})
|
|
12644
|
+
};
|
|
12645
|
+
}
|
|
13273
12646
|
async function remoteHistory(record, flags) {
|
|
13274
12647
|
const rootRecord = assertRootScoped(record, "history");
|
|
13275
12648
|
await refreshPresence(rootRecord, record.docId);
|
|
@@ -13329,9 +12702,11 @@ async function refreshPresence(record, docId) {
|
|
|
13329
12702
|
}
|
|
13330
12703
|
async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
13331
12704
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
13332
|
-
|
|
12705
|
+
const rootRecord = rootScopedRecordFor(record, document.rootId);
|
|
12706
|
+
await refreshPresence(rootRecord, document.docId);
|
|
12707
|
+
const authority = flags["no-review"] ? void 0 : await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
|
|
13333
12708
|
if (flags.summary || flags["metadata-only"]) {
|
|
13334
|
-
return withRemoteSummaryNextCommands(
|
|
12709
|
+
return withRemoteSummaryNextCommands(summarizeDocumentContextFromAuthority(document, authority), record, document, pathOrDocId);
|
|
13335
12710
|
}
|
|
13336
12711
|
const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
|
|
13337
12712
|
const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
|
|
@@ -13347,22 +12722,24 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
13347
12722
|
startLine: sl,
|
|
13348
12723
|
endLine: el,
|
|
13349
12724
|
markdown,
|
|
13350
|
-
...flags["no-review"] ? {} :
|
|
13351
|
-
comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
13352
|
-
suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
13353
|
-
anchors: document.anchors
|
|
13354
|
-
},
|
|
12725
|
+
...flags["no-review"] ? {} : authorityReviewPayload(authority),
|
|
13355
12726
|
images: document.images,
|
|
13356
12727
|
links: document.links
|
|
13357
12728
|
};
|
|
13358
12729
|
}
|
|
13359
12730
|
async function remoteReview(record, pathOrDocId) {
|
|
13360
12731
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
13361
|
-
|
|
12732
|
+
const rootRecord = rootScopedRecordFor(record, document.rootId);
|
|
12733
|
+
await refreshPresence(rootRecord, document.docId);
|
|
12734
|
+
const authority = await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
|
|
13362
12735
|
return {
|
|
13363
12736
|
joinId: record.joinId,
|
|
13364
12737
|
scope: record.scope,
|
|
13365
|
-
|
|
12738
|
+
docId: document.docId,
|
|
12739
|
+
path: document.path,
|
|
12740
|
+
title: document.title,
|
|
12741
|
+
currentSha: document.currentSha,
|
|
12742
|
+
...authorityReviewPayload(authority)
|
|
13366
12743
|
};
|
|
13367
12744
|
}
|
|
13368
12745
|
function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
|
|
@@ -13382,6 +12759,48 @@ function remoteCommandTarget(record, document, requestedTarget) {
|
|
|
13382
12759
|
if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
|
|
13383
12760
|
return ` ${document.docId}`;
|
|
13384
12761
|
}
|
|
12762
|
+
function summarizeDocumentContextFromAuthority(document, authority) {
|
|
12763
|
+
const summary = summarizeDocumentContext(document);
|
|
12764
|
+
return {
|
|
12765
|
+
...summary,
|
|
12766
|
+
reviewCounts: authorityReviewCounts(authority)
|
|
12767
|
+
};
|
|
12768
|
+
}
|
|
12769
|
+
function authorityReviewPayload(authority) {
|
|
12770
|
+
return {
|
|
12771
|
+
comments: authorityOpenComments(authority),
|
|
12772
|
+
suggestions: authorityOpenSuggestions(authority),
|
|
12773
|
+
commentTargets: authority?.commentProjections ?? []
|
|
12774
|
+
};
|
|
12775
|
+
}
|
|
12776
|
+
function authorityReviewCounts(authority) {
|
|
12777
|
+
const suggestions = authorityOpenSuggestions(authority);
|
|
12778
|
+
const comments = authorityOpenComments(authority);
|
|
12779
|
+
return {
|
|
12780
|
+
openComments: comments.length,
|
|
12781
|
+
openSuggestions: suggestions.length,
|
|
12782
|
+
anchorsNeedingReview: (authority?.commentProjections ?? []).filter((projection) => authorityRecordStatus(projection) !== "mapped").length + suggestions.filter((suggestion) => {
|
|
12783
|
+
const projection = suggestion && typeof suggestion === "object" ? suggestion.projection : void 0;
|
|
12784
|
+
const status = authorityRecordStatus(projection);
|
|
12785
|
+
return status === "conflicted" || status === "stale";
|
|
12786
|
+
}).length
|
|
12787
|
+
};
|
|
12788
|
+
}
|
|
12789
|
+
function authorityOpenComments(authority) {
|
|
12790
|
+
return (authority?.comments ?? []).filter((comment2) => {
|
|
12791
|
+
const status = authorityRecordStatus(comment2);
|
|
12792
|
+
return status === void 0 || status === "open" || status === "displaced";
|
|
12793
|
+
});
|
|
12794
|
+
}
|
|
12795
|
+
function authorityOpenSuggestions(authority) {
|
|
12796
|
+
return (authority?.suggestions ?? []).filter((suggestion) => authorityRecordStatus(suggestion) === "open");
|
|
12797
|
+
}
|
|
12798
|
+
function authorityRecordId(value) {
|
|
12799
|
+
return value && typeof value === "object" && typeof value.id === "string" ? value.id : void 0;
|
|
12800
|
+
}
|
|
12801
|
+
function authorityRecordStatus(value) {
|
|
12802
|
+
return value && typeof value === "object" && typeof value.status === "string" ? value.status : void 0;
|
|
12803
|
+
}
|
|
13385
12804
|
async function remoteCreateFile(root, record, parsed) {
|
|
13386
12805
|
assertProjectScope(record, "create-file");
|
|
13387
12806
|
const rootRecord = assertRootScoped(record, "create-file");
|
|
@@ -13402,7 +12821,7 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
13402
12821
|
}
|
|
13403
12822
|
const docId = createDocId();
|
|
13404
12823
|
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
13405
|
-
const
|
|
12824
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13406
12825
|
const baseDocument = {
|
|
13407
12826
|
workspaceId: rootRecord.workspaceId,
|
|
13408
12827
|
rootId: rootRecord.rootId,
|
|
@@ -13410,16 +12829,15 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
13410
12829
|
path: normalizedPath,
|
|
13411
12830
|
title,
|
|
13412
12831
|
markdown,
|
|
13413
|
-
reviewMarkdown: markdown,
|
|
13414
|
-
sidecar,
|
|
13415
|
-
anchors: [],
|
|
13416
12832
|
images: [],
|
|
13417
12833
|
links: [],
|
|
13418
12834
|
openComments: 0,
|
|
13419
12835
|
openSuggestions: 0,
|
|
12836
|
+
anchorsNeedingReview: 0,
|
|
12837
|
+
updatedAt: now,
|
|
13420
12838
|
currentSha: tree.root.canonical.head
|
|
13421
12839
|
};
|
|
13422
|
-
const document = await pushDocument(rootRecord, baseDocument, markdown
|
|
12840
|
+
const document = await pushDocument(rootRecord, baseDocument, markdown);
|
|
13423
12841
|
await recordHead(root, record, document);
|
|
13424
12842
|
return { document };
|
|
13425
12843
|
}
|
|
@@ -13444,8 +12862,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
13444
12862
|
const moved = await pushDocument(
|
|
13445
12863
|
rootScopedRecordFor(record, document.rootId),
|
|
13446
12864
|
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
13447
|
-
document.markdown
|
|
13448
|
-
document.sidecar
|
|
12865
|
+
document.markdown
|
|
13449
12866
|
);
|
|
13450
12867
|
await recordHead(root, record, moved);
|
|
13451
12868
|
return { document: moved };
|
|
@@ -13453,13 +12870,17 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
13453
12870
|
async function remoteComment(root, record, parsed) {
|
|
13454
12871
|
const body = await readRequiredTextFlag(parsed.flags, root, ["body", "message"]);
|
|
13455
12872
|
const range = parseRange(requiredFlag(parsed.flags, "range"));
|
|
13456
|
-
const
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
12873
|
+
const pathOrDocId = parsed.command[2] ?? record.docId;
|
|
12874
|
+
if (!pathOrDocId) {
|
|
12875
|
+
throw new CliError("usage_error", "Missing document path or docId.", {
|
|
12876
|
+
hint: "Pass a document path or docId, or join with --doc <docId>."
|
|
12877
|
+
});
|
|
12878
|
+
}
|
|
12879
|
+
const document = await fetchDocument(record, pathOrDocId);
|
|
12880
|
+
const documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12881
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12882
|
+
const result = await postAuthorityComment(documentRecord, document.docId, { ...range, body }, actorForRecord(record));
|
|
12883
|
+
return { comment: result.thread, projection: result.projection, document: { docId: document.docId, path: document.path, currentSha: document.currentSha } };
|
|
13463
12884
|
}
|
|
13464
12885
|
async function remoteSuggest(root, record, parsed) {
|
|
13465
12886
|
const replacement = await readRequiredTextFlag(parsed.flags, root, ["with", "replacement"]);
|
|
@@ -13474,15 +12895,35 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
13474
12895
|
const document = await fetchDocument(record, pathOrDocId);
|
|
13475
12896
|
const documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13476
12897
|
await refreshPresence(documentRecord, document.docId);
|
|
13477
|
-
const
|
|
12898
|
+
const authority = await fetchAuthorityState(documentRecord, document.docId);
|
|
12899
|
+
if (!authority.head) {
|
|
12900
|
+
throw new CliError("conflict", "Document authority is not initialized yet.", {
|
|
12901
|
+
hint: "Open the document once or retry after the worker initializes authority-state."
|
|
12902
|
+
});
|
|
12903
|
+
}
|
|
12904
|
+
const clientMutationId = typeof parsed.flags["client-mutation-id"] === "string" ? parsed.flags["client-mutation-id"] : typeof parsed.flags["mutation-id"] === "string" ? parsed.flags["mutation-id"] : void 0;
|
|
12905
|
+
const patch = buildLineRangePatchEnvelope({
|
|
12906
|
+
baseVersion: authority.head.headVersion,
|
|
12907
|
+
base: authority.markdown ?? document.markdown,
|
|
12908
|
+
range,
|
|
12909
|
+
replacement,
|
|
12910
|
+
clientMutationId,
|
|
12911
|
+
clientMeta: { source: "cli_remote_suggest" }
|
|
12912
|
+
});
|
|
12913
|
+
const result = await postSuggestionBranch(
|
|
13478
12914
|
documentRecord,
|
|
13479
12915
|
document.docId,
|
|
13480
|
-
{
|
|
12916
|
+
{ patch, title: message },
|
|
13481
12917
|
actorForRecord(record)
|
|
13482
12918
|
);
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
12919
|
+
return {
|
|
12920
|
+
suggestion: result.suggestion,
|
|
12921
|
+
suggestionUpdate: result.update,
|
|
12922
|
+
suggestionId: result.suggestion?.id,
|
|
12923
|
+
projectionStatus: result.projection?.status,
|
|
12924
|
+
replayed: result.replayed,
|
|
12925
|
+
document: { docId: document.docId, path: document.path, currentSha: document.currentSha }
|
|
12926
|
+
};
|
|
13486
12927
|
}
|
|
13487
12928
|
async function remoteCreateFolder(record, parsed) {
|
|
13488
12929
|
assertProjectScope(record, "create-folder");
|
|
@@ -13561,53 +13002,14 @@ async function remoteReject(root, record, parsed) {
|
|
|
13561
13002
|
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13562
13003
|
await refreshPresence(documentRecord, document.docId);
|
|
13563
13004
|
const actor = actorForRecord(record);
|
|
13564
|
-
const
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
});
|
|
13569
|
-
}
|
|
13570
|
-
if (suggestion.status !== "open") {
|
|
13571
|
-
throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestion.status}.`, {
|
|
13005
|
+
const authority = await fetchAuthorityState(documentRecord, document.docId).catch(() => void 0);
|
|
13006
|
+
const suggestionStatus = authorityRecordStatus(authority?.suggestions?.find((candidate) => authorityRecordId(candidate) === suggestionId));
|
|
13007
|
+
if (suggestionStatus && suggestionStatus !== "open") {
|
|
13008
|
+
throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestionStatus}.`, {
|
|
13572
13009
|
hint: "Only open suggestions can be rejected."
|
|
13573
13010
|
});
|
|
13574
13011
|
}
|
|
13575
|
-
|
|
13576
|
-
documentRecord,
|
|
13577
|
-
document.docId,
|
|
13578
|
-
{ kind: "reject_suggestion", baseHead: document.currentSha, payload: { suggestionId } },
|
|
13579
|
-
actor
|
|
13580
|
-
);
|
|
13581
|
-
if (result.document) await recordHead(root, record, result.document);
|
|
13582
|
-
return { suggestion: result.suggestion ?? { ...suggestion, status: "rejected" }, reviewRecord: result.reviewRecord, document: result.document };
|
|
13583
|
-
}
|
|
13584
|
-
async function submitReview(root, record, pathOrDocId, mutate) {
|
|
13585
|
-
if (!pathOrDocId) {
|
|
13586
|
-
throw new CliError("usage_error", "Missing document path or docId.", {
|
|
13587
|
-
hint: "Pass a document path or docId, or join with --doc <docId>."
|
|
13588
|
-
});
|
|
13589
|
-
}
|
|
13590
|
-
let document = await fetchDocument(record, pathOrDocId);
|
|
13591
|
-
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13592
|
-
await refreshPresence(documentRecord, document.docId);
|
|
13593
|
-
const build = async (base) => {
|
|
13594
|
-
const io = new RemoteDocumentIO(base);
|
|
13595
|
-
const created = await mutate(io, base.docId);
|
|
13596
|
-
const state = await getDocumentState(io, base.docId);
|
|
13597
|
-
return {
|
|
13598
|
-
created,
|
|
13599
|
-
state,
|
|
13600
|
-
additions: {
|
|
13601
|
-
anchors: state.sidecar.anchors.filter((anchor) => anchor.id === created.anchorId),
|
|
13602
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.id === created.id),
|
|
13603
|
-
reviewMarkdown: state.sidecar.reviewMarkdown
|
|
13604
|
-
}
|
|
13605
|
-
};
|
|
13606
|
-
};
|
|
13607
|
-
const attempt = await build(document);
|
|
13608
|
-
const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
|
|
13609
|
-
await recordHead(root, record, pushed);
|
|
13610
|
-
return { created: attempt.created, document: pushed };
|
|
13012
|
+
return postResolveSuggestionBranch(documentRecord, document.docId, suggestionId, "reject", actor);
|
|
13611
13013
|
}
|
|
13612
13014
|
async function recordHead(root, record, document) {
|
|
13613
13015
|
await writeJoinRecord(root, { ...record, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), currentHead: document.currentSha });
|
|
@@ -13737,19 +13139,6 @@ function rootScopedRecordFor(record, rootId) {
|
|
|
13737
13139
|
scope: record.scope === "file" ? "file" : "project"
|
|
13738
13140
|
};
|
|
13739
13141
|
}
|
|
13740
|
-
function emptySidecar(docId, path, title) {
|
|
13741
|
-
return {
|
|
13742
|
-
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
13743
|
-
docId,
|
|
13744
|
-
path,
|
|
13745
|
-
title,
|
|
13746
|
-
anchors: [],
|
|
13747
|
-
comments: [],
|
|
13748
|
-
suggestions: [],
|
|
13749
|
-
changeSets: [],
|
|
13750
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13751
|
-
};
|
|
13752
|
-
}
|
|
13753
13142
|
function defaultMarkdown(path) {
|
|
13754
13143
|
return `# ${titleFromMarkdown(path, "")}
|
|
13755
13144
|
`;
|
|
@@ -13780,13 +13169,54 @@ function optionalFolderTarget(value) {
|
|
|
13780
13169
|
|
|
13781
13170
|
// src/bridge.ts
|
|
13782
13171
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
13783
|
-
import {
|
|
13172
|
+
import { unlinkSync } from "node:fs";
|
|
13173
|
+
import { chmod, mkdir as mkdir4, readFile as readFile6, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13784
13174
|
import { homedir } from "node:os";
|
|
13785
13175
|
import { basename as basename2, join as join4, resolve as resolve4 } from "node:path";
|
|
13786
13176
|
var BRIDGE_CONFIG_PATH = ".mdocs/bridge.json";
|
|
13787
13177
|
var BRIDGE_STATUS_PATH = ".mdocs/bridge-status.json";
|
|
13178
|
+
var BRIDGE_LOCK_PATH = ".mdocs/bridge.lock";
|
|
13788
13179
|
var BRIDGE_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
13789
13180
|
var BRIDGE_TOKEN_STORE_PATH = join4(homedir(), ".mdocs", "bridge-tokens.json");
|
|
13181
|
+
var BRIDGE_MIN_INTERVAL_MS = 500;
|
|
13182
|
+
var BRIDGE_DEFAULT_INTERVAL_MS = 1e3;
|
|
13183
|
+
var BRIDGE_RECONNECT_BASE_MS = 1e3;
|
|
13184
|
+
var BRIDGE_RECONNECT_CAP_MS = 3e4;
|
|
13185
|
+
function createSerialQueue() {
|
|
13186
|
+
let tail = Promise.resolve();
|
|
13187
|
+
return (task) => {
|
|
13188
|
+
const run = tail.then(task, task);
|
|
13189
|
+
tail = run.then(
|
|
13190
|
+
() => void 0,
|
|
13191
|
+
() => void 0
|
|
13192
|
+
);
|
|
13193
|
+
return run;
|
|
13194
|
+
};
|
|
13195
|
+
}
|
|
13196
|
+
function validateBridgeIntervalMs(raw) {
|
|
13197
|
+
if (typeof raw === "boolean" || raw === void 0 || raw === null || raw === "") return BRIDGE_DEFAULT_INTERVAL_MS;
|
|
13198
|
+
const value = Number(raw);
|
|
13199
|
+
if (!Number.isFinite(value) || value <= 0) return BRIDGE_DEFAULT_INTERVAL_MS;
|
|
13200
|
+
return Math.max(BRIDGE_MIN_INTERVAL_MS, Math.floor(value));
|
|
13201
|
+
}
|
|
13202
|
+
function reconnectBackoffCeiling(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS) {
|
|
13203
|
+
const exponent = Math.max(0, Math.floor(attempt));
|
|
13204
|
+
return Math.min(capMs, baseMs * 2 ** exponent);
|
|
13205
|
+
}
|
|
13206
|
+
function reconnectDelayMs(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS, random = Math.random) {
|
|
13207
|
+
return Math.floor(random() * (reconnectBackoffCeiling(attempt, baseMs, capMs) + 1));
|
|
13208
|
+
}
|
|
13209
|
+
function isConfirmingEcho(params) {
|
|
13210
|
+
if (params.messageSourceId && params.messageSourceId === params.selfSourceId) return true;
|
|
13211
|
+
return params.sentSignature !== void 0 && params.sentSignature === params.payloadSignature;
|
|
13212
|
+
}
|
|
13213
|
+
function shouldConflictCopyOnDelete(localSignature, baseSignature) {
|
|
13214
|
+
return baseSignature === void 0 || localSignature !== baseSignature;
|
|
13215
|
+
}
|
|
13216
|
+
function conflictCopyStamp(date = /* @__PURE__ */ new Date(), entropy = randomUUID3().slice(0, 6)) {
|
|
13217
|
+
const compact = date.toISOString().replaceAll(/[-:.]/g, "");
|
|
13218
|
+
return `${compact.slice(0, 15)}-${entropy}`;
|
|
13219
|
+
}
|
|
13790
13220
|
function bridgeSetupIdentity(input) {
|
|
13791
13221
|
const actorName = input.actorName?.trim() || "Agent";
|
|
13792
13222
|
return {
|
|
@@ -13874,21 +13304,33 @@ async function runBridge(options) {
|
|
|
13874
13304
|
const pendingDocs = /* @__PURE__ */ new Set();
|
|
13875
13305
|
const pendingDeletes = /* @__PURE__ */ new Set();
|
|
13876
13306
|
const seenDocs = /* @__PURE__ */ new Set();
|
|
13307
|
+
const sentSignatures = /* @__PURE__ */ new Map();
|
|
13877
13308
|
const deniedSignatures = /* @__PURE__ */ new Map();
|
|
13309
|
+
const indexCache = /* @__PURE__ */ new Map();
|
|
13310
|
+
const indexOptions = { cache: indexCache, rootId };
|
|
13311
|
+
const runManifest = createSerialQueue();
|
|
13878
13312
|
let lastTreeSignature = "";
|
|
13879
13313
|
const claimMode = Boolean(options.claimToken || localManifestSource?.canonicalHead);
|
|
13880
13314
|
let lastAppliedHead = localManifestSource?.canonicalHead;
|
|
13881
13315
|
let socket;
|
|
13316
|
+
let pollTimer;
|
|
13317
|
+
let reconnectTimer;
|
|
13318
|
+
let ticking = false;
|
|
13319
|
+
let reconnectAttempt = 0;
|
|
13320
|
+
let suppressOpenResume = false;
|
|
13321
|
+
let claimPrimed = false;
|
|
13882
13322
|
let lastSuccessfulSyncAt;
|
|
13883
13323
|
let lastHeartbeatAt;
|
|
13884
13324
|
let lastHeartbeatSentMs = 0;
|
|
13885
13325
|
let lastError;
|
|
13326
|
+
const lock = await acquireBridgeLock(root);
|
|
13886
13327
|
await writeCurrentBridgeStatus("starting");
|
|
13887
13328
|
let token = options.token ?? await readStoredBridgeToken(options, rootId).catch(() => void 0);
|
|
13888
13329
|
if (!token && options.resume && !options.requestToken) {
|
|
13889
13330
|
const message = "Missing stored bridge token. Re-run mdocs bridge setup from a fresh Magic binding packet.";
|
|
13890
13331
|
lastError = message;
|
|
13891
13332
|
await writeCurrentBridgeStatus("error");
|
|
13333
|
+
await lock.release();
|
|
13892
13334
|
throw new Error(message);
|
|
13893
13335
|
}
|
|
13894
13336
|
if (!token && (options.claimToken || options.requestToken)) {
|
|
@@ -13899,6 +13341,7 @@ async function runBridge(options) {
|
|
|
13899
13341
|
} catch (error) {
|
|
13900
13342
|
lastError = errorMessage(error);
|
|
13901
13343
|
await writeCurrentBridgeStatus("error");
|
|
13344
|
+
await lock.release();
|
|
13902
13345
|
throw error;
|
|
13903
13346
|
}
|
|
13904
13347
|
}
|
|
@@ -13908,6 +13351,7 @@ async function runBridge(options) {
|
|
|
13908
13351
|
} catch (error) {
|
|
13909
13352
|
lastError = errorMessage(error);
|
|
13910
13353
|
await writeCurrentBridgeStatus("error");
|
|
13354
|
+
await lock.release();
|
|
13911
13355
|
throw error;
|
|
13912
13356
|
}
|
|
13913
13357
|
lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
|
|
@@ -13927,32 +13371,54 @@ async function runBridge(options) {
|
|
|
13927
13371
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13928
13372
|
});
|
|
13929
13373
|
const backfilled = await resumeFromCanonical();
|
|
13374
|
+
suppressOpenResume = true;
|
|
13930
13375
|
if (options.once) {
|
|
13931
13376
|
lastSuccessfulSyncAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13932
13377
|
await writeCurrentBridgeStatus("once_complete");
|
|
13378
|
+
await lock.release();
|
|
13933
13379
|
return;
|
|
13934
13380
|
}
|
|
13381
|
+
if (claimMode && !backfilled) {
|
|
13382
|
+
await primeLocalSignatures();
|
|
13383
|
+
claimPrimed = true;
|
|
13384
|
+
}
|
|
13935
13385
|
connect();
|
|
13936
|
-
|
|
13937
|
-
else await publishSnapshot("initial");
|
|
13938
|
-
const timer = setInterval(() => {
|
|
13939
|
-
void publishSnapshot("poll").catch((error) => {
|
|
13940
|
-
lastError = errorMessage(error);
|
|
13941
|
-
void writeCurrentBridgeStatus("error").catch(() => void 0);
|
|
13942
|
-
sendHeartbeat("error", lastError);
|
|
13943
|
-
process.stderr.write(`bridge poll failed: ${lastError}
|
|
13944
|
-
`);
|
|
13945
|
-
});
|
|
13946
|
-
}, options.intervalMs);
|
|
13386
|
+
scheduleNextPoll();
|
|
13947
13387
|
process.once("SIGINT", () => {
|
|
13948
|
-
|
|
13388
|
+
shutdownTimers();
|
|
13949
13389
|
sendHeartbeat("offline", void 0, false);
|
|
13950
13390
|
void writeCurrentBridgeStatus("stopped").catch(() => void 0).finally(() => {
|
|
13951
13391
|
socket?.close();
|
|
13952
|
-
process.exit(0);
|
|
13392
|
+
void lock.release().finally(() => process.exit(0));
|
|
13953
13393
|
});
|
|
13954
13394
|
});
|
|
13955
13395
|
await new Promise(() => void 0);
|
|
13396
|
+
function shutdownTimers() {
|
|
13397
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
13398
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
13399
|
+
}
|
|
13400
|
+
function scheduleNextPoll() {
|
|
13401
|
+
pollTimer = setTimeout(runPollTick, options.intervalMs);
|
|
13402
|
+
}
|
|
13403
|
+
async function runPollTick() {
|
|
13404
|
+
if (ticking) {
|
|
13405
|
+
scheduleNextPoll();
|
|
13406
|
+
return;
|
|
13407
|
+
}
|
|
13408
|
+
ticking = true;
|
|
13409
|
+
try {
|
|
13410
|
+
await publishSnapshot("poll");
|
|
13411
|
+
} catch (error) {
|
|
13412
|
+
lastError = errorMessage(error);
|
|
13413
|
+
void writeCurrentBridgeStatus("error").catch(() => void 0);
|
|
13414
|
+
sendHeartbeat("error", lastError);
|
|
13415
|
+
process.stderr.write(`bridge poll failed: ${lastError}
|
|
13416
|
+
`);
|
|
13417
|
+
} finally {
|
|
13418
|
+
ticking = false;
|
|
13419
|
+
scheduleNextPoll();
|
|
13420
|
+
}
|
|
13421
|
+
}
|
|
13956
13422
|
function connect() {
|
|
13957
13423
|
const url = new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl);
|
|
13958
13424
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -13961,11 +13427,10 @@ async function runBridge(options) {
|
|
|
13961
13427
|
if (token) url.searchParams.set("token", token);
|
|
13962
13428
|
socket = new WebSocket(url);
|
|
13963
13429
|
socket.addEventListener("open", () => {
|
|
13964
|
-
|
|
13965
|
-
|
|
13430
|
+
void onSocketOpen().catch((error) => {
|
|
13431
|
+
process.stderr.write(`bridge open handler failed: ${errorMessage(error)}
|
|
13966
13432
|
`);
|
|
13967
|
-
|
|
13968
|
-
void writeCurrentBridgeStatus("connected").catch(() => void 0);
|
|
13433
|
+
});
|
|
13969
13434
|
});
|
|
13970
13435
|
socket.addEventListener("message", (event) => {
|
|
13971
13436
|
if (typeof event.data !== "string") return;
|
|
@@ -13979,7 +13444,7 @@ async function runBridge(options) {
|
|
|
13979
13444
|
socket.addEventListener("close", () => {
|
|
13980
13445
|
socket = void 0;
|
|
13981
13446
|
void writeCurrentBridgeStatus("offline").catch(() => void 0);
|
|
13982
|
-
|
|
13447
|
+
scheduleReconnect();
|
|
13983
13448
|
});
|
|
13984
13449
|
socket.addEventListener("error", () => {
|
|
13985
13450
|
lastError = "WebSocket error";
|
|
@@ -13987,8 +13452,34 @@ async function runBridge(options) {
|
|
|
13987
13452
|
socket?.close();
|
|
13988
13453
|
});
|
|
13989
13454
|
}
|
|
13455
|
+
function scheduleReconnect() {
|
|
13456
|
+
const delayMs = reconnectDelayMs(reconnectAttempt);
|
|
13457
|
+
reconnectAttempt += 1;
|
|
13458
|
+
reconnectTimer = setTimeout(connect, delayMs);
|
|
13459
|
+
}
|
|
13460
|
+
async function onSocketOpen() {
|
|
13461
|
+
lastError = void 0;
|
|
13462
|
+
reconnectAttempt = 0;
|
|
13463
|
+
process.stdout.write(`mdocs bridge connected ${root} -> ${options.workspaceId}/${rootId}
|
|
13464
|
+
`);
|
|
13465
|
+
sendHeartbeat("synced");
|
|
13466
|
+
void writeCurrentBridgeStatus("connected").catch(() => void 0);
|
|
13467
|
+
pendingDocs.clear();
|
|
13468
|
+
pendingDeletes.clear();
|
|
13469
|
+
sentSignatures.clear();
|
|
13470
|
+
lastTreeSignature = "";
|
|
13471
|
+
if (suppressOpenResume) suppressOpenResume = false;
|
|
13472
|
+
else await resumeFromCanonical();
|
|
13473
|
+
await publishSnapshot("open");
|
|
13474
|
+
}
|
|
13475
|
+
function indexedMap() {
|
|
13476
|
+
return runManifest(() => getWorkspaceMap(io, indexOptions));
|
|
13477
|
+
}
|
|
13478
|
+
function indexedDocState(pathOrDocId) {
|
|
13479
|
+
return runManifest(() => getDocumentState(io, pathOrDocId, indexOptions));
|
|
13480
|
+
}
|
|
13990
13481
|
async function publishSnapshot(reason) {
|
|
13991
|
-
const map = await
|
|
13482
|
+
const map = await indexedMap();
|
|
13992
13483
|
const currentDocIds = new Set(map.docs.map((doc) => doc.docId));
|
|
13993
13484
|
const treeSignature = hashJson(map.docs.map((doc) => ({ docId: doc.docId, path: doc.path })));
|
|
13994
13485
|
if (treeSignature !== lastTreeSignature) {
|
|
@@ -13997,19 +13488,21 @@ async function runBridge(options) {
|
|
|
13997
13488
|
}
|
|
13998
13489
|
}
|
|
13999
13490
|
for (const doc of map.docs) {
|
|
14000
|
-
const state = await
|
|
13491
|
+
const state = await indexedDocState(doc.docId);
|
|
14001
13492
|
const payload = {
|
|
14002
13493
|
...state,
|
|
14003
13494
|
workspaceId: options.workspaceId,
|
|
14004
13495
|
rootId,
|
|
13496
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14005
13497
|
currentSha: lastAppliedHead
|
|
14006
13498
|
};
|
|
14007
|
-
const signature = documentSignature(state.markdown
|
|
13499
|
+
const signature = documentSignature(state.markdown);
|
|
14008
13500
|
if (signatures.get(doc.docId) === signature || pendingDocs.has(doc.docId)) continue;
|
|
14009
13501
|
if (deniedSignatures.get(doc.docId) === signature) continue;
|
|
14010
13502
|
if (send("file-changed", { ...payload, baseHead: lastAppliedHead, replicaId })) {
|
|
14011
13503
|
seenDocs.add(doc.docId);
|
|
14012
13504
|
pendingDocs.add(doc.docId);
|
|
13505
|
+
sentSignatures.set(doc.docId, signature);
|
|
14013
13506
|
}
|
|
14014
13507
|
}
|
|
14015
13508
|
for (const docId of seenDocs) {
|
|
@@ -14055,10 +13548,10 @@ async function runBridge(options) {
|
|
|
14055
13548
|
return true;
|
|
14056
13549
|
}
|
|
14057
13550
|
async function reconcileRemoteDocument(remote, base) {
|
|
14058
|
-
const remoteSignature = documentSignature(remote.markdown
|
|
14059
|
-
const baseSignature = base ? documentSignature(base.markdown
|
|
13551
|
+
const remoteSignature = documentSignature(remote.markdown);
|
|
13552
|
+
const baseSignature = base ? documentSignature(base.markdown) : void 0;
|
|
14060
13553
|
const localState = await localStateForCanonical(remote);
|
|
14061
|
-
const localSignature = localState ? documentSignature(localState.markdown
|
|
13554
|
+
const localSignature = localState ? documentSignature(localState.markdown) : void 0;
|
|
14062
13555
|
if (!localState) {
|
|
14063
13556
|
await applyCanonicalDocument(remote);
|
|
14064
13557
|
return "applied";
|
|
@@ -14086,24 +13579,37 @@ async function runBridge(options) {
|
|
|
14086
13579
|
return "conflict";
|
|
14087
13580
|
}
|
|
14088
13581
|
async function reconcileRemoteDelete(docId, base) {
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
if (
|
|
13582
|
+
return applyCanonicalDelete(docId, base.path, documentSignature(base.markdown));
|
|
13583
|
+
}
|
|
13584
|
+
async function applyCanonicalDelete(docId, path, baseSignature) {
|
|
13585
|
+
let localState = await indexedDocState(docId).catch(() => void 0);
|
|
13586
|
+
if (!localState && path) {
|
|
13587
|
+
const map = await indexedMap().catch(() => void 0);
|
|
13588
|
+
const byPath = map?.docs.find((doc) => doc.path === path);
|
|
13589
|
+
if (byPath) localState = await indexedDocState(byPath.docId).catch(() => void 0);
|
|
13590
|
+
}
|
|
13591
|
+
if (!localState) {
|
|
13592
|
+
signatures.delete(docId);
|
|
13593
|
+
seenDocs.delete(docId);
|
|
13594
|
+
pendingDeletes.delete(docId);
|
|
13595
|
+
return "noop";
|
|
13596
|
+
}
|
|
13597
|
+
const localSignature = documentSignature(localState.markdown);
|
|
13598
|
+
const diverged = shouldConflictCopyOnDelete(localSignature, baseSignature);
|
|
13599
|
+
if (diverged) {
|
|
14094
13600
|
const copyPath = conflictCopyPath(localState.path);
|
|
14095
13601
|
await io.writeTextAtomic(copyPath, localState.markdown);
|
|
14096
|
-
await
|
|
13602
|
+
await seedConflictCopyDocument(copyPath, localState.markdown);
|
|
14097
13603
|
process.stderr.write(`local conflict ${localState.path}; local version saved as ${copyPath}, canonical delete applied
|
|
14098
13604
|
`);
|
|
14099
13605
|
}
|
|
14100
13606
|
await io.deleteFile(localState.path).catch(() => void 0);
|
|
14101
|
-
await io.deleteFile(sidecarPath(docId)).catch(() => void 0);
|
|
14102
13607
|
await removeManifestDocument(docId, localState.path);
|
|
14103
13608
|
signatures.delete(docId);
|
|
14104
13609
|
seenDocs.delete(docId);
|
|
13610
|
+
sentSignatures.delete(docId);
|
|
14105
13611
|
pendingDeletes.delete(docId);
|
|
14106
|
-
return
|
|
13612
|
+
return diverged ? "conflict" : "applied";
|
|
14107
13613
|
}
|
|
14108
13614
|
async function handleIncoming(message) {
|
|
14109
13615
|
if (message.type === "agent-unbound") {
|
|
@@ -14112,23 +13618,36 @@ async function runBridge(options) {
|
|
|
14112
13618
|
if (!targetsUs) return;
|
|
14113
13619
|
process.stdout.write(`mdocs bridge unbound by workspace; stopping ${root}
|
|
14114
13620
|
`);
|
|
13621
|
+
shutdownTimers();
|
|
14115
13622
|
await writeCurrentBridgeStatus("stopped").catch(() => void 0);
|
|
14116
13623
|
socket?.close();
|
|
13624
|
+
await lock.release();
|
|
14117
13625
|
process.exit(0);
|
|
14118
13626
|
}
|
|
14119
13627
|
if (message.type === "file-changed") {
|
|
14120
|
-
|
|
13628
|
+
const payload = readDocumentPayload(message.payload);
|
|
13629
|
+
const payloadSignature = documentSignature(payload.markdown);
|
|
13630
|
+
if (isConfirmingEcho({
|
|
13631
|
+
messageSourceId: message.sourceId,
|
|
13632
|
+
selfSourceId: sourceId,
|
|
13633
|
+
payloadSignature,
|
|
13634
|
+
sentSignature: sentSignatures.get(payload.docId)
|
|
13635
|
+
})) {
|
|
13636
|
+
signatures.set(payload.docId, payloadSignature);
|
|
13637
|
+
sentSignatures.delete(payload.docId);
|
|
13638
|
+
pendingDocs.delete(payload.docId);
|
|
13639
|
+
deniedSignatures.delete(payload.docId);
|
|
13640
|
+
seenDocs.add(payload.docId);
|
|
13641
|
+
lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
|
|
13642
|
+
await writeSourceState(lastAppliedHead);
|
|
13643
|
+
return;
|
|
13644
|
+
}
|
|
13645
|
+
await applyCanonicalDocument(payload);
|
|
14121
13646
|
}
|
|
14122
13647
|
if (message.type === "file-deleted") {
|
|
14123
13648
|
const payload = message.payload;
|
|
14124
13649
|
if (!payload.docId) return;
|
|
14125
|
-
|
|
14126
|
-
const doc = map.docs.find((candidate) => candidate.docId === payload.docId);
|
|
14127
|
-
if (doc) await io.deleteFile(doc.path);
|
|
14128
|
-
await io.deleteFile(sidecarPath(payload.docId)).catch(() => void 0);
|
|
14129
|
-
signatures.delete(payload.docId);
|
|
14130
|
-
seenDocs.delete(payload.docId);
|
|
14131
|
-
pendingDeletes.delete(payload.docId);
|
|
13650
|
+
await applyCanonicalDelete(payload.docId, payload.path, signatures.get(payload.docId));
|
|
14132
13651
|
lastAppliedHead = payload.canonicalHead ?? lastAppliedHead;
|
|
14133
13652
|
await writeSourceState(lastAppliedHead);
|
|
14134
13653
|
process.stdout.write(`applied canonical delete ${payload.docId} at ${lastAppliedHead ?? "unknown"}
|
|
@@ -14156,14 +13675,14 @@ async function runBridge(options) {
|
|
|
14156
13675
|
}
|
|
14157
13676
|
}
|
|
14158
13677
|
async function applyCanonicalDocument(payload) {
|
|
14159
|
-
const remoteSignature = documentSignature(payload.markdown
|
|
13678
|
+
const remoteSignature = documentSignature(payload.markdown);
|
|
14160
13679
|
const localState = await localStateForCanonical(payload);
|
|
14161
|
-
const localSignature = localState ? documentSignature(localState.markdown
|
|
13680
|
+
const localSignature = localState ? documentSignature(localState.markdown) : void 0;
|
|
14162
13681
|
if (localSignature && signatures.has(payload.docId) && localSignature !== signatures.get(payload.docId) && localSignature !== remoteSignature) {
|
|
14163
13682
|
const copyPath = conflictCopyPath(payload.path);
|
|
14164
13683
|
if (localState) {
|
|
14165
13684
|
await io.writeTextAtomic(copyPath, localState.markdown);
|
|
14166
|
-
await
|
|
13685
|
+
await seedConflictCopyDocument(copyPath, localState.markdown);
|
|
14167
13686
|
}
|
|
14168
13687
|
send("conflict", {
|
|
14169
13688
|
docId: payload.docId,
|
|
@@ -14180,21 +13699,13 @@ async function runBridge(options) {
|
|
|
14180
13699
|
await io.deleteFile(localState.path).catch(() => void 0);
|
|
14181
13700
|
}
|
|
14182
13701
|
if (localState && localState.docId !== payload.docId) {
|
|
14183
|
-
await io.deleteFile(sidecarPath(localState.docId)).catch(() => void 0);
|
|
14184
13702
|
await removeManifestDocument(localState.docId, localState.path);
|
|
14185
13703
|
}
|
|
14186
13704
|
await io.writeTextAtomic(payload.path, payload.markdown);
|
|
14187
|
-
await io.writeTextAtomic(sidecarPath(payload.docId), `${JSON.stringify(payload.sidecar, null, 2)}
|
|
14188
|
-
`);
|
|
14189
|
-
const reviewMarkdown = projectReviewMarkdown(payload.markdown, payload.sidecar.suggestions);
|
|
14190
|
-
if (reviewMarkdown === payload.markdown) {
|
|
14191
|
-
await io.deleteFile(reviewFilePath(payload.path)).catch(() => void 0);
|
|
14192
|
-
} else {
|
|
14193
|
-
await io.writeTextAtomic(reviewFilePath(payload.path), reviewMarkdown);
|
|
14194
|
-
}
|
|
14195
13705
|
await upsertManifestDocument(payload);
|
|
14196
13706
|
signatures.set(payload.docId, remoteSignature);
|
|
14197
13707
|
deniedSignatures.delete(payload.docId);
|
|
13708
|
+
sentSignatures.delete(payload.docId);
|
|
14198
13709
|
seenDocs.add(payload.docId);
|
|
14199
13710
|
pendingDocs.delete(payload.docId);
|
|
14200
13711
|
lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
|
|
@@ -14202,38 +13713,33 @@ async function runBridge(options) {
|
|
|
14202
13713
|
process.stdout.write(`applied canonical change ${payload.path} at ${lastAppliedHead ?? "unknown"}
|
|
14203
13714
|
`);
|
|
14204
13715
|
}
|
|
14205
|
-
async function
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
});
|
|
14225
|
-
await writeManifest(io, {
|
|
14226
|
-
...manifest,
|
|
14227
|
-
docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
|
|
14228
|
-
updatedAt: now
|
|
13716
|
+
async function seedConflictCopyDocument(copyPath, markdown) {
|
|
13717
|
+
await runManifest(async () => {
|
|
13718
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13719
|
+
if (!manifest) return;
|
|
13720
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13721
|
+
const docId = deterministicDocId(rootId, copyPath);
|
|
13722
|
+
const title = titleFromMarkdown(copyPath, markdown);
|
|
13723
|
+
const entry = {
|
|
13724
|
+
docId,
|
|
13725
|
+
path: copyPath,
|
|
13726
|
+
title,
|
|
13727
|
+
contentHash: contentHashForText(markdown),
|
|
13728
|
+
updatedAt: now
|
|
13729
|
+
};
|
|
13730
|
+
await writeManifest(io, {
|
|
13731
|
+
...manifest,
|
|
13732
|
+
docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
|
|
13733
|
+
updatedAt: now
|
|
13734
|
+
});
|
|
14229
13735
|
});
|
|
14230
13736
|
}
|
|
14231
13737
|
async function localStateForCanonical(payload) {
|
|
14232
|
-
const byDocId = await
|
|
13738
|
+
const byDocId = await indexedDocState(payload.docId).catch(() => void 0);
|
|
14233
13739
|
if (byDocId) return byDocId;
|
|
14234
|
-
const map = await
|
|
13740
|
+
const map = await indexedMap().catch(() => void 0);
|
|
14235
13741
|
const byPath = map?.docs.find((doc) => doc.path === payload.path);
|
|
14236
|
-
return byPath ?
|
|
13742
|
+
return byPath ? indexedDocState(byPath.docId).catch(() => void 0) : void 0;
|
|
14237
13743
|
}
|
|
14238
13744
|
async function fetchCanonicalDocument2(docId) {
|
|
14239
13745
|
try {
|
|
@@ -14330,59 +13836,65 @@ async function runBridge(options) {
|
|
|
14330
13836
|
await writeBridgeStatus(root, currentBridgeStatus(state));
|
|
14331
13837
|
}
|
|
14332
13838
|
async function primeLocalSignatures() {
|
|
14333
|
-
const map = await
|
|
13839
|
+
const map = await indexedMap();
|
|
14334
13840
|
for (const doc of map.docs) {
|
|
14335
|
-
const state = await
|
|
14336
|
-
signatures.set(doc.docId, documentSignature(state.markdown
|
|
13841
|
+
const state = await indexedDocState(doc.docId);
|
|
13842
|
+
signatures.set(doc.docId, documentSignature(state.markdown));
|
|
14337
13843
|
seenDocs.add(doc.docId);
|
|
14338
13844
|
}
|
|
14339
13845
|
process.stdout.write(`mdocs bridge claimed ${root} as ${rootId} at ${lastAppliedHead ?? "unknown"}
|
|
14340
13846
|
`);
|
|
14341
13847
|
}
|
|
14342
13848
|
async function localSignatureFor(docId) {
|
|
14343
|
-
const state = await
|
|
14344
|
-
return documentSignature(state.markdown
|
|
13849
|
+
const state = await indexedDocState(docId);
|
|
13850
|
+
return documentSignature(state.markdown);
|
|
14345
13851
|
}
|
|
14346
13852
|
async function writeSourceState(head) {
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
13853
|
+
await runManifest(async () => {
|
|
13854
|
+
const manifest = await indexWorkspace(io, indexOptions);
|
|
13855
|
+
await writeManifest(io, {
|
|
13856
|
+
...manifest,
|
|
13857
|
+
source: {
|
|
13858
|
+
sourceId: rootId,
|
|
13859
|
+
sourceName: options.sourceName ?? (basename2(root) || rootId),
|
|
13860
|
+
canonicalHead: head,
|
|
13861
|
+
claimedAt: manifest.source?.claimedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
13862
|
+
mappings: [{ ...mapping, lastAppliedHead: head, status: head ? "synced" : "connecting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
13863
|
+
},
|
|
13864
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13865
|
+
});
|
|
14358
13866
|
});
|
|
14359
13867
|
}
|
|
14360
13868
|
async function upsertManifestDocument(payload) {
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
13869
|
+
await runManifest(async () => {
|
|
13870
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13871
|
+
if (!manifest) return;
|
|
13872
|
+
await writeManifest(io, {
|
|
13873
|
+
...manifest,
|
|
13874
|
+
docs: [
|
|
13875
|
+
...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
|
|
13876
|
+
{
|
|
13877
|
+
docId: payload.docId,
|
|
13878
|
+
path: payload.path,
|
|
13879
|
+
title: payload.title || titleFromMarkdown(payload.path, payload.markdown),
|
|
13880
|
+
contentHash: contentHashForText(payload.markdown),
|
|
13881
|
+
currentSha: payload.canonicalHead ?? payload.currentSha,
|
|
13882
|
+
updatedAt: payload.updatedAt
|
|
13883
|
+
}
|
|
13884
|
+
].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
|
|
13885
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13886
|
+
});
|
|
14377
13887
|
});
|
|
14378
13888
|
}
|
|
14379
13889
|
async function removeManifestDocument(docId, path) {
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
13890
|
+
await runManifest(async () => {
|
|
13891
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13892
|
+
if (!manifest) return;
|
|
13893
|
+
await writeManifest(io, {
|
|
13894
|
+
...manifest,
|
|
13895
|
+
docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
|
|
13896
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13897
|
+
});
|
|
14386
13898
|
});
|
|
14387
13899
|
}
|
|
14388
13900
|
}
|
|
@@ -14573,7 +14085,7 @@ async function fetchCanonicalDocument(options, rootId, docId) {
|
|
|
14573
14085
|
function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
|
|
14574
14086
|
if (!payload || typeof payload !== "object") return void 0;
|
|
14575
14087
|
const value = payload;
|
|
14576
|
-
if (typeof value.markdown !== "string"
|
|
14088
|
+
if (typeof value.markdown !== "string") return void 0;
|
|
14577
14089
|
return readDocumentPayload({
|
|
14578
14090
|
workspaceId,
|
|
14579
14091
|
rootId,
|
|
@@ -14581,7 +14093,7 @@ function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
|
|
|
14581
14093
|
path: value.path,
|
|
14582
14094
|
title: value.title,
|
|
14583
14095
|
markdown: value.markdown,
|
|
14584
|
-
|
|
14096
|
+
updatedAt: value.updatedAt,
|
|
14585
14097
|
currentSha: headId,
|
|
14586
14098
|
canonicalHead: headId
|
|
14587
14099
|
});
|
|
@@ -14704,18 +14216,65 @@ function mergeReplicas(replicas, replica) {
|
|
|
14704
14216
|
function rootIdForPath(path) {
|
|
14705
14217
|
return `root_${createHash2("sha256").update(path).digest("hex").slice(0, 12)}`;
|
|
14706
14218
|
}
|
|
14707
|
-
function conflictCopyPath(path) {
|
|
14708
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "").slice(0, 15);
|
|
14219
|
+
function conflictCopyPath(path, stamp = conflictCopyStamp()) {
|
|
14709
14220
|
return path.replace(/(\.[^./]+)?$/, (extension) => `.conflict-${stamp}${extension || ".md"}`);
|
|
14710
14221
|
}
|
|
14711
|
-
function
|
|
14712
|
-
|
|
14222
|
+
async function acquireBridgeLock(root) {
|
|
14223
|
+
const lockPath = join4(root, BRIDGE_LOCK_PATH);
|
|
14224
|
+
await mkdir4(join4(root, ".mdocs"), { recursive: true });
|
|
14225
|
+
const payload = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
14226
|
+
`;
|
|
14227
|
+
try {
|
|
14228
|
+
await writeFile4(lockPath, payload, { flag: "wx" });
|
|
14229
|
+
} catch (error) {
|
|
14230
|
+
if (error.code !== "EEXIST") throw error;
|
|
14231
|
+
const holderPid = await readBridgeLockPid(lockPath);
|
|
14232
|
+
if (holderPid !== void 0 && isProcessAlive(holderPid)) {
|
|
14233
|
+
throw new Error(
|
|
14234
|
+
`Another mdocs bridge (pid ${holderPid}) is already running for ${root}. Stop it first or remove ${lockPath} if it is stale.`
|
|
14235
|
+
);
|
|
14236
|
+
}
|
|
14237
|
+
await writeFile4(lockPath, payload);
|
|
14238
|
+
}
|
|
14239
|
+
let released = false;
|
|
14240
|
+
const removeSync = () => {
|
|
14241
|
+
if (released) return;
|
|
14242
|
+
released = true;
|
|
14243
|
+
try {
|
|
14244
|
+
unlinkSync(lockPath);
|
|
14245
|
+
} catch {
|
|
14246
|
+
}
|
|
14247
|
+
};
|
|
14248
|
+
process.once("exit", removeSync);
|
|
14249
|
+
return {
|
|
14250
|
+
release: async () => {
|
|
14251
|
+
if (released) return;
|
|
14252
|
+
released = true;
|
|
14253
|
+
await rm3(lockPath, { force: true }).catch(() => void 0);
|
|
14254
|
+
}
|
|
14255
|
+
};
|
|
14256
|
+
}
|
|
14257
|
+
async function readBridgeLockPid(lockPath) {
|
|
14258
|
+
try {
|
|
14259
|
+
const parsed = JSON.parse(await readFile6(lockPath, "utf8"));
|
|
14260
|
+
return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : void 0;
|
|
14261
|
+
} catch {
|
|
14262
|
+
return void 0;
|
|
14263
|
+
}
|
|
14264
|
+
}
|
|
14265
|
+
function isProcessAlive(pid) {
|
|
14266
|
+
try {
|
|
14267
|
+
process.kill(pid, 0);
|
|
14268
|
+
return true;
|
|
14269
|
+
} catch (error) {
|
|
14270
|
+
return error.code === "EPERM";
|
|
14271
|
+
}
|
|
14713
14272
|
}
|
|
14714
14273
|
function hashJson(value) {
|
|
14715
14274
|
return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14716
14275
|
}
|
|
14717
|
-
function documentSignature(markdown
|
|
14718
|
-
return hashJson({ markdown
|
|
14276
|
+
function documentSignature(markdown) {
|
|
14277
|
+
return hashJson({ markdown });
|
|
14719
14278
|
}
|
|
14720
14279
|
async function readLocalSource(root) {
|
|
14721
14280
|
const io = new NodeWorkspaceIO(root);
|
|
@@ -14731,12 +14290,12 @@ function readDocumentPayload(payload) {
|
|
|
14731
14290
|
if (typeof value.docId !== "string") throw new Error("file-changed payload missing docId");
|
|
14732
14291
|
if (typeof value.path !== "string") throw new Error("file-changed payload missing path");
|
|
14733
14292
|
if (typeof value.markdown !== "string") throw new Error("file-changed payload missing markdown");
|
|
14734
|
-
if (!value.sidecar || typeof value.sidecar !== "object") throw new Error("file-changed payload missing sidecar");
|
|
14735
14293
|
return {
|
|
14736
14294
|
docId: value.docId,
|
|
14737
14295
|
path: value.path,
|
|
14296
|
+
title: typeof value.title === "string" ? value.title : titleFromMarkdown(value.path, value.markdown),
|
|
14738
14297
|
markdown: value.markdown,
|
|
14739
|
-
|
|
14298
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
14740
14299
|
currentSha: typeof value.currentSha === "string" ? value.currentSha : void 0,
|
|
14741
14300
|
canonicalHead: typeof value.canonicalHead === "string" ? value.canonicalHead : void 0
|
|
14742
14301
|
};
|
|
@@ -15036,11 +14595,7 @@ async function main() {
|
|
|
15036
14595
|
...image2.workspacePath ? { absolutePath: resolve6(cwd, image2.workspacePath) } : {}
|
|
15037
14596
|
})),
|
|
15038
14597
|
links: state.links,
|
|
15039
|
-
...parsed.flags["no-review"] ? {} : {
|
|
15040
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
15041
|
-
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
15042
|
-
anchors: state.anchors
|
|
15043
|
-
}
|
|
14598
|
+
...parsed.flags["no-review"] ? {} : { review: reviewStateForDocument(state) }
|
|
15044
14599
|
}, parsed.flags);
|
|
15045
14600
|
return;
|
|
15046
14601
|
}
|
|
@@ -15050,49 +14605,25 @@ async function main() {
|
|
|
15050
14605
|
return;
|
|
15051
14606
|
}
|
|
15052
14607
|
case "comments": {
|
|
15053
|
-
|
|
15054
|
-
print(state.sidecar.comments, parsed.flags);
|
|
15055
|
-
return;
|
|
14608
|
+
throw localReviewCommandError("comments");
|
|
15056
14609
|
}
|
|
15057
14610
|
case "comment": {
|
|
15058
|
-
|
|
15059
|
-
const range = parseRange2(requiredFlag2(parsed.flags, "range"));
|
|
15060
|
-
const body = await readRequiredTextFlag2(parsed.flags, cwd, ["body", "message"]);
|
|
15061
|
-
if (!body) throw new CliError("usage_error", "Comment body is empty.", { hint: "Pass --body <text> or a non-empty --body-file." });
|
|
15062
|
-
print(await addComment(io, path, range, body, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15063
|
-
return;
|
|
14611
|
+
throw localReviewCommandError("comment");
|
|
15064
14612
|
}
|
|
15065
14613
|
case "suggestions": {
|
|
15066
|
-
|
|
15067
|
-
print(state.sidecar.suggestions, parsed.flags);
|
|
15068
|
-
return;
|
|
14614
|
+
throw localReviewCommandError("suggestions");
|
|
15069
14615
|
}
|
|
15070
14616
|
case "suggest": {
|
|
15071
|
-
|
|
15072
|
-
const range = parseRange2(requiredFlag2(parsed.flags, "range"));
|
|
15073
|
-
const replacement = await readRequiredTextFlag2(parsed.flags, cwd, ["with", "replacement"]);
|
|
15074
|
-
const message = await readOptionalTextFlag2(parsed.flags, cwd, ["message"]) ?? "Suggested edit";
|
|
15075
|
-
print(await addSuggestion(io, path, range, replacement, message, actorFromFlags(parsed.flags, {
|
|
15076
|
-
id: "agent_local",
|
|
15077
|
-
kind: "agent",
|
|
15078
|
-
name: "Local Agent"
|
|
15079
|
-
})), parsed.flags);
|
|
15080
|
-
return;
|
|
14617
|
+
throw localReviewCommandError("suggest");
|
|
15081
14618
|
}
|
|
15082
14619
|
case "accept": {
|
|
15083
|
-
|
|
15084
|
-
print(await acceptSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15085
|
-
return;
|
|
14620
|
+
throw localReviewCommandError("accept");
|
|
15086
14621
|
}
|
|
15087
14622
|
case "reject": {
|
|
15088
|
-
|
|
15089
|
-
print(await rejectSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15090
|
-
return;
|
|
14623
|
+
throw localReviewCommandError("reject");
|
|
15091
14624
|
}
|
|
15092
14625
|
case "resolve-comment": {
|
|
15093
|
-
|
|
15094
|
-
print(await resolveComment(io, commentId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15095
|
-
return;
|
|
14626
|
+
throw localReviewCommandError("resolve-comment");
|
|
15096
14627
|
}
|
|
15097
14628
|
case "sync":
|
|
15098
14629
|
case "status": {
|
|
@@ -15149,7 +14680,7 @@ async function main() {
|
|
|
15149
14680
|
claimToken: typeof parsed.flags.claim === "string" ? parsed.flags.claim : void 0,
|
|
15150
14681
|
actorId: typeof parsed.flags.actor === "string" ? parsed.flags.actor : void 0,
|
|
15151
14682
|
actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
|
|
15152
|
-
intervalMs:
|
|
14683
|
+
intervalMs: validateBridgeIntervalMs(parsed.flags.interval),
|
|
15153
14684
|
token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
|
|
15154
14685
|
requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
|
|
15155
14686
|
pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0,
|
|
@@ -15176,9 +14707,15 @@ async function main() {
|
|
|
15176
14707
|
});
|
|
15177
14708
|
return;
|
|
15178
14709
|
}
|
|
14710
|
+
const workspaceId = typeof parsed.flags.workspace === "string" && parsed.flags.workspace.trim() ? parsed.flags.workspace.trim() : void 0;
|
|
14711
|
+
if (!workspaceId) {
|
|
14712
|
+
throw new CliError("usage_error", "Missing --workspace <id> for mdocs bridge.", {
|
|
14713
|
+
hint: "Pass the workspace explicitly, for example --workspace ws_123. The bridge no longer assumes a demo workspace."
|
|
14714
|
+
});
|
|
14715
|
+
}
|
|
15179
14716
|
const options = {
|
|
15180
14717
|
...baseOptions,
|
|
15181
|
-
workspaceId
|
|
14718
|
+
workspaceId,
|
|
15182
14719
|
baseUrl: bridgeBaseUrl(parsed.flags)
|
|
15183
14720
|
};
|
|
15184
14721
|
if (subcommand === "setup") await runBridgeSetup(options);
|
|
@@ -15264,23 +14801,14 @@ function parseArgs(args) {
|
|
|
15264
14801
|
}
|
|
15265
14802
|
return { command, flags };
|
|
15266
14803
|
}
|
|
15267
|
-
function parseRange2(value) {
|
|
15268
|
-
const [start, end = start] = value.split(":").map(Number);
|
|
15269
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || !start || !end || start < 1 || end < start) {
|
|
15270
|
-
throw new CliError("invalid_range", `Invalid --range value "${value}".`, {
|
|
15271
|
-
hint: "Use --range <startLine>:<endLine> with 1-based inclusive line numbers, for example --range 4:9."
|
|
15272
|
-
});
|
|
15273
|
-
}
|
|
15274
|
-
return { startLine: start, endLine: end };
|
|
15275
|
-
}
|
|
15276
14804
|
function requiredArg(value, name) {
|
|
15277
14805
|
if (!value) throw new CliError("usage_error", `Missing <${name}> argument.`);
|
|
15278
14806
|
return value;
|
|
15279
14807
|
}
|
|
15280
|
-
function
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
14808
|
+
function localReviewCommandError(command) {
|
|
14809
|
+
return new CliError("usage_error", `Local review command "${command}" was removed.`, {
|
|
14810
|
+
hint: "Use `mdocs remote ...` or the remote MCP connector so comments, suggestions, accept, and reject go through DocumentAuthority."
|
|
14811
|
+
});
|
|
15284
14812
|
}
|
|
15285
14813
|
function bridgeBaseUrl(flags) {
|
|
15286
14814
|
const configured = optionalBridgeBaseUrl(flags);
|
|
@@ -15294,40 +14822,6 @@ function optionalBridgeBaseUrl(flags) {
|
|
|
15294
14822
|
const configured = process.env.MDOCS_BASE_URL?.trim();
|
|
15295
14823
|
return configured || void 0;
|
|
15296
14824
|
}
|
|
15297
|
-
async function readRequiredTextFlag2(flags, cwd, names) {
|
|
15298
|
-
const value = await readOptionalTextFlag2(flags, cwd, names);
|
|
15299
|
-
if (value === void 0) {
|
|
15300
|
-
throw new CliError("usage_error", `Pass --${names[0]} <text> or --${names[0]}-file <path>.`, {
|
|
15301
|
-
hint: `For multiline text, write it to a file and pass --${names[0]}-file.`
|
|
15302
|
-
});
|
|
15303
|
-
}
|
|
15304
|
-
return value;
|
|
15305
|
-
}
|
|
15306
|
-
async function readOptionalTextFlag2(flags, cwd, names) {
|
|
15307
|
-
for (const name of names) {
|
|
15308
|
-
const fileValue = flags[`${name}-file`];
|
|
15309
|
-
if (typeof fileValue === "string") return readFile8(resolve6(cwd, fileValue), "utf8");
|
|
15310
|
-
const value = flags[name];
|
|
15311
|
-
if (typeof value === "string") return value;
|
|
15312
|
-
}
|
|
15313
|
-
return void 0;
|
|
15314
|
-
}
|
|
15315
|
-
function actorFromFlags(flags, fallback) {
|
|
15316
|
-
const id = typeof flags.actor === "string" ? flags.actor : fallback.id;
|
|
15317
|
-
const name = typeof flags["actor-name"] === "string" ? flags["actor-name"] : fallback.name;
|
|
15318
|
-
const email = typeof flags["actor-email"] === "string" ? flags["actor-email"] : fallback.email;
|
|
15319
|
-
const kind = actorKindFromFlag(flags["actor-kind"]) ?? fallback.kind;
|
|
15320
|
-
return {
|
|
15321
|
-
id,
|
|
15322
|
-
kind,
|
|
15323
|
-
name,
|
|
15324
|
-
...email ? { email } : {}
|
|
15325
|
-
};
|
|
15326
|
-
}
|
|
15327
|
-
function actorKindFromFlag(value) {
|
|
15328
|
-
if (value === "human" || value === "agent" || value === "system") return value;
|
|
15329
|
-
return void 0;
|
|
15330
|
-
}
|
|
15331
14825
|
function print(value, flags) {
|
|
15332
14826
|
if (flags.json || typeof value !== "string") {
|
|
15333
14827
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
@@ -15347,20 +14841,14 @@ Commands:
|
|
|
15347
14841
|
init [path] Initialize .mdocs and index Markdown files
|
|
15348
14842
|
map --json Print workspace map
|
|
15349
14843
|
graph --json Print workspace knowledge graph nodes and edges
|
|
15350
|
-
state <path> --json Print
|
|
14844
|
+
state <path> --json Print clean Markdown document state
|
|
15351
14845
|
context <path> --json Print agent-ready context
|
|
15352
14846
|
context <path> --summary --json Print document metadata and headings
|
|
15353
|
-
review <path> --json Print
|
|
15354
|
-
comments
|
|
15355
|
-
comment
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
suggest <path> --range 3:5 --with-file replacement.md
|
|
15359
|
-
Add a multiline reviewMarkdown suggestion
|
|
15360
|
-
accept <suggestionId> Apply and accept a suggestion
|
|
15361
|
-
reject <suggestionId> Reject a suggestion
|
|
15362
|
-
resolve-comment <commentId> Resolve a comment thread
|
|
15363
|
-
status --json Print Git status and sidecar map
|
|
14847
|
+
review <path> --json Print local authority-review redirect metadata
|
|
14848
|
+
comments|comment|suggestions Removed locally; use remote authority commands
|
|
14849
|
+
suggest|accept|reject|resolve-comment
|
|
14850
|
+
Removed locally; use remote authority commands
|
|
14851
|
+
status --json Print Git status and workspace map
|
|
15364
14852
|
sync --json Alias for status until hosted sync is configured
|
|
15365
14853
|
diff [path] Print local Git diff
|
|
15366
14854
|
history [path] --json Print local Git history
|
|
@@ -15393,7 +14881,7 @@ Commands:
|
|
|
15393
14881
|
(or set MDOCS_BRIDGE_TOKEN)
|
|
15394
14882
|
bridge --claim <token> --root . --canonical-prefix prompts --replica-prefix packages/agent/prompts
|
|
15395
14883
|
Claim an existing repo path as a Magic-canonical source mapping
|
|
15396
|
-
doctor --json Validate
|
|
14884
|
+
doctor --json Validate workspace map and agent readiness
|
|
15397
14885
|
serve-mcp Start JSON-RPC MCP-compatible stdio server
|
|
15398
14886
|
version [--json] Print CLI version
|
|
15399
14887
|
help <command> Show usage, examples, and notes for one command
|